From 029b7631c7f0f4093cd8721ac120990ca0610f83 Mon Sep 17 00:00:00 2001
From: gluesniffler <159397573+gluesniffler@users.noreply.github.com>
Date: Sat, 16 Nov 2024 20:32:30 -0400
Subject: [PATCH] Shitmed: Implementing Existing Newmed Code Into SS14 (#1159)
# Description
## The mythical surgery system. Heard whispered in hushed tones in the
corners of [REDACTED], it has been real since 2 years ago. If you listen
very carefully you might be able to hear the sound of arachne crashing
the server.
Jokes aside. Lets set some expectations, ideally this will not mess too
much with existing body code, besides trying to not die from all the
fucking test fails, all the while introducing needed systems for
handling wounds, surgery, part/organ manipulation, and displaying all of
those to the players.
The PR in its current state **is** working properly, you can pick it up
and get surgery on your server today, though of course its buggy due to
the unhandled issues it has right now. If you do pick it up, give me a
heads up and I'll see if I can help you out.
This PR is mostly intended as a public roasting ground for my shitcode,
so that other contribs/maints can pitch in to help improve it as well.
---
# TODO
- [ ] FIX MY FUCKING SHITCODE AAAAAAAAAAAAAAAAAAAAAAAAAA
- [x] Have fun :)
---
# Demo/Walkthrough
[![Surgery
Demo](https://i.ytimg.com/vi/UhxS5b3LC-A/maxresdefault.jpg)](https://www.youtube.com/watch?v=UhxS5b3LC-A
"Surgery Demo")
---
# Tasks currently being worked on:
- [x] Porting an upgraded body doll that is less shit to use.
- [x] Add a completely innocuous felinization/defelinization surgery.
(Highly sought after feature :D )
- [x] Implement pizza limb sprites and add em as a surgery (soon :tm:)
- [ ] Add CyberneticsSystem
- [ ] Add a series of cybernetic limbs with special properties, and
different susceptibilities to EMPs
- [ ] Add the associated surgeries to cybernetic implants and their
associated maintenance.
- [ ] Tweak Cybernetics Traits to use CyberneticsSystem, and overwrite
the entity's limbs on spawn (I LOVE SHITCODE)
- [ ] Add Cybernetic Limbs to Research
- [x] Start adding a shitload of Shitmed comments on wherever I made
changes, since we're getting fairly close to what I could call a stable
v1
- [ ] Refactor SurgeryBUI to be slightly less shitcodey, and properly
use BUI states instead of a half-assed BUIMessage.
- [ ] Separate harpy wings into two distinct wings rather than a single
layer.
---
# Reported bugs that I haven't been able to replicate
- Apparently returning to the body kicks you back to the body instead of
the entity that the brain is attached to?
- Disconnecting and reconnecting after a brain/head transplant makes the
client crash. Seems to be related to identity.
- Transplanting a head sometimes makes it so that you cannot strip other
entities.
---
# Changelog
:cl: Mocho
- add: A week has passed. Surgery is here.
---------
Signed-off-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com>
Co-authored-by: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com>
Co-authored-by: goet <6637097+goet@users.noreply.github.com>
Co-authored-by: Saphire Lattice
---
.../Body/Components/BrainComponent.cs | 3 +
.../Body/Components/LungComponent.cs | 3 +
.../Body/Components/StomachComponent.cs | 3 +
Content.Client/Body/Systems/BodySystem.cs | 65 ++
Content.Client/Hands/Systems/HandsSystem.cs | 35 +-
.../UI/HealthAnalyzerBoundUserInterface.cs | 7 +
.../UI/HealthAnalyzerWindow.xaml | 220 ++++-
.../UI/HealthAnalyzerWindow.xaml.cs | 126 ++-
.../Humanoid/HumanoidAppearanceSystem.cs | 8 +-
Content.Client/Input/ContentContexts.cs | 6 +
.../Inventory/ClientInventorySystem.cs | 13 +-
.../Inventory/InventorySlotsComponent.cs | 2 +-
Content.Client/Medical/Surgery/SurgeryBui.cs | 358 +++++++++
.../Medical/Surgery/SurgeryStepButton.xaml | 4 +
.../Medical/Surgery/SurgeryStepButton.xaml.cs | 16 +
.../Medical/Surgery/SurgerySystem.cs | 11 +
.../Medical/Surgery/SurgeryWindow.xaml | 23 +
.../Medical/Surgery/SurgeryWindow.xaml.cs | 14 +
.../Options/UI/Tabs/KeyRebindTab.xaml.cs | 8 +
Content.Client/Stylesheets/StyleNano.cs | 54 ++
Content.Client/Targeting/TargetingSystem.cs | 102 +++
.../Screens/OverlayChatGameScreen.xaml | 2 +
.../Screens/OverlayChatGameScreen.xaml.cs | 1 +
.../Screens/SeparatedChatGameScreen.xaml | 2 +
.../Screens/SeparatedChatGameScreen.xaml.cs | 1 +
.../Systems/Alerts/Widgets/AlertsUI.xaml | 10 +-
.../Inventory/InventoryUIController.cs | 4 +-
.../PartStatus/PartStatusUIController.cs | 82 ++
.../PartStatus/Widgets/PartStatusControl.xaml | 57 ++
.../Widgets/PartStatusControl.xaml.cs | 50 ++
.../Targeting/TargetingUIController.cs | 82 ++
.../Targeting/Widgets/TargetingControl.xaml | 216 +++++
.../Widgets/TargetingControl.xaml.cs | 58 ++
.../Xenonids/UI/XenoChoiceControl.xaml | 17 +
.../Xenonids/UI/XenoChoiceControl.xaml.cs | 26 +
.../Tests/Shitmed/Body/SpeciesBUiTest.cs | 63 ++
.../Atmos/EntitySystems/BarotraumaSystem.cs | 4 +-
Content.Server/Bed/Sleep/SleepingSystem.cs | 7 +-
Content.Server/Body/Systems/BodySystem.cs | 57 +-
Content.Server/Body/Systems/BrainSystem.cs | 24 +-
.../Body/Systems/RespiratorSystem.cs | 3 +-
.../Chemistry/ReagentEffects/HealthChange.cs | 8 +-
.../Thresholds/Behaviors/GibBehavior.cs | 5 +-
.../Thresholds/Behaviors/GibPartBehavior.cs | 19 +
Content.Server/Execution/ExecutionSystem.cs | 4 +-
Content.Server/Hands/Systems/HandsSystem.cs | 51 +-
.../Components/HealthAnalyzerComponent.cs | 6 +
Content.Server/Medical/CryoPodSystem.cs | 1 +
Content.Server/Medical/HealingSystem.cs | 28 +-
.../Medical/HealthAnalyzerSystem.cs | 71 +-
.../Medical/Surgery/SurgerySystem.cs | 189 +++++
Content.Server/Targeting/TargetingSystem.cs | 54 ++
.../Traits/Assorted/LightweightDrunkSystem.cs | 2 +-
.../Body/Events/AmputateAttemptEvent.cs | 7 +
.../Body/Organ/DebrainedComponent.cs | 7 +
Content.Shared/Body/Organ/EarsComponent.cs | 6 +
Content.Shared/Body/Organ/EyesComponent.cs | 6 +
Content.Shared/Body/Organ/HeartComponent.cs | 6 +
Content.Shared/Body/Organ/LiverComponent.cs | 6 +
.../Body/Organ/MarkingContainerComponent.cs | 15 +
Content.Shared/Body/Organ/OrganComponent.cs | 20 +-
Content.Shared/Body/Organ/TailComponent.cs | 6 +
.../Body/Part/BodyPartAppearanceComponent.cs | 45 ++
Content.Shared/Body/Part/BodyPartComponent.cs | 111 ++-
Content.Shared/Body/Part/BodyPartEvents.cs | 20 +
.../Body/Systems/SharedBodySystem.Body.cs | 99 ++-
.../Body/Systems/SharedBodySystem.Organs.cs | 11 +
.../SharedBodySystem.PartAppearance.cs | 198 +++++
.../Body/Systems/SharedBodySystem.Parts.cs | 270 ++++++-
.../Systems/SharedBodySystem.Targeting.cs | 503 ++++++++++++
.../Body/Systems/SharedBodySystem.cs | 5 +-
Content.Shared/CCVar/CCVars.cs | 6 +
.../Damage/Systems/DamageableSystem.cs | 54 +-
.../Hands/Components/HandsComponent.cs | 1 +
.../Hands/EntitySystems/SharedHandsSystem.cs | 2 +
.../Events/ProfileLoadFinishedEvent.cs | 7 +
.../Humanoid/HumanoidVisualLayersExtension.cs | 12 +
.../SharedHumanoidAppearanceSystem.cs | 2 +
Content.Shared/Input/ContentKeyFunctions.cs | 6 +
.../Inventory/InventoryComponent.cs | 1 +
.../Inventory/InventorySystem.Slots.cs | 52 +-
.../Inventory/InventoryTemplatePrototype.cs | 5 +
.../SurgeryCloseIncisionConditionComponent.cs | 6 +
.../SurgeryLarvaConditionComponent.cs | 6 +
.../SurgeryMarkingConditionComponent.cs | 27 +
...SurgeryOperatingTableConditionComponent.cs | 6 +
.../SurgeryOrganConditionComponent.cs | 18 +
.../SurgeryPartConditionComponent.cs | 17 +
.../Conditions/SurgeryPartPresentCondition.cs | 6 +
.../SurgeryPartRemovedConditionComponent.cs | 14 +
.../Surgery/Conditions/SurgeryValidEvent.cs | 9 +
.../SurgeryWoundedConditionComponent.cs | 7 +
.../Effects/Complete/SurgeryCompletedEvent.cs | 7 +
.../Complete/SurgeryRemoveLarvaComponent.cs | 6 +
.../SurgeryDamageChangeEffectComponent.cs | 17 +
...rgerySpecialDamageChangeEffectComponent.cs | 14 +
.../Step/SurgeryStepCavityEffectComponent.cs | 10 +
.../Step/SurgeryStepEmoteEffectComponent.cs | 12 +
.../Effects/Step/SurgeryStepSpawnEffect.cs | 13 +
.../Step/SurgeryTendWoundsEffectComponent.cs | 20 +
.../Surgery/OperatingTableComponent.cs | 6 +
.../Surgery/SharedSurgerySystem.Steps.cs | 755 ++++++++++++++++++
.../Medical/Surgery/SharedSurgerySystem.cs | 283 +++++++
.../Medical/Surgery/StepInvalidReason.cs | 10 +
.../Steps/Parts/BleedersClampedComponent.cs | 6 +
.../Parts/BodyPartReattachedComponent.cs | 6 +
.../Steps/Parts/BodyPartSawedComponent.cs | 6 +
.../Steps/Parts/IncisionOpenComponent.cs | 6 +
.../Parts/InternalBleedersClampedComponent.cs | 6 +
.../Steps/Parts/OrganReattachedComponent.cs | 6 +
.../Steps/Parts/PartRemovedComponent.cs | 6 +
.../Steps/Parts/RibcageOpenComponent.cs | 6 +
.../Steps/Parts/RibcageSawedComponent.cs | 6 +
.../Steps/Parts/SkinRetractedComponent.cs | 6 +
.../Steps/SurgeryAddMarkingStepComponent.cs | 34 +
.../Steps/SurgeryAddOrganStepComponent.cs | 6 +
.../Steps/SurgeryAddPartStepComponent.cs | 6 +
.../Steps/SurgeryAffixOrganStepComponent.cs | 6 +
.../Steps/SurgeryAffixPartStepComponent.cs | 6 +
.../Steps/SurgeryCanPerformStepEvent.cs | 14 +
.../SurgeryCutLarvaRootsStepComponent.cs | 6 +
.../SurgeryRemoveMarkingStepComponent.cs | 29 +
.../Steps/SurgeryRemoveOrganStepComponent.cs | 6 +
.../Steps/SurgeryRemovePartStepComponent.cs | 6 +
.../Steps/SurgeryRepeatableStepComponent.cs | 6 +
.../Steps/SurgeryStepCompleteCheckEvent.cs | 4 +
.../Surgery/Steps/SurgeryStepComponent.cs | 22 +
.../Medical/Surgery/SurgeryComponent.cs | 18 +
.../Medical/Surgery/SurgeryDoAfterEvent.cs | 18 +
.../Surgery/SurgerySpeedModifierComponent.cs | 11 +
.../Medical/Surgery/SurgeryStepDamageEvent.cs | 9 +
.../Medical/Surgery/SurgeryStepEvent.cs | 7 +
.../Medical/Surgery/SurgeryTargetComponent.cs | 10 +
Content.Shared/Medical/Surgery/SurgeryUI.cs | 32 +
.../Medical/Surgery/SurgeryUiRefreshEvent.cs | 14 +
.../Medical/Surgery/Tools/BoneGelComponent.cs | 11 +
.../Medical/Surgery/Tools/BoneSawComponent.cs | 10 +
.../Surgery/Tools/BoneSetterComponent.cs | 6 +
.../Medical/Surgery/Tools/CauteryComponent.cs | 10 +
.../Surgery/Tools/HemostatComponent.cs | 10 +
.../Surgery/Tools/ISurgeryToolComponent.cs | 11 +
.../Surgery/Tools/RetractorComponent.cs | 10 +
.../Medical/Surgery/Tools/ScalpelComponent.cs | 10 +
.../Surgery/Tools/SurgeryToolComponent.cs | 16 +
.../Surgery/Tools/SurgicalDrillComponent.cs | 10 +
.../HealthAnalyzerScannedUserMessage.cs | 16 +-
.../Systems/MobStateSystem.StateMachine.cs | 4 +
.../Standing/SharedLayingDownSystem.cs | 6 +-
Content.Shared/Targeting/Events.cs | 38 +
.../Targeting/SharedTargetingSystem.cs | 26 +
Content.Shared/Targeting/TargetBodyPart.cs | 31 +
Content.Shared/Targeting/TargetIntegrity.cs | 13 +
.../Targeting/TargetingComponent.cs | 59 ++
.../Weapons/Melee/MeleeWeaponComponent.cs | 12 +
.../Weapons/Melee/SharedMeleeWeaponSystem.cs | 4 +-
.../Audio/Medical/Surgery/attributions.yml | 49 ++
Resources/Audio/Medical/Surgery/cautery1.ogg | Bin 0 -> 34770 bytes
Resources/Audio/Medical/Surgery/cautery2.ogg | Bin 0 -> 16854 bytes
Resources/Audio/Medical/Surgery/hemostat1.ogg | Bin 0 -> 15729 bytes
Resources/Audio/Medical/Surgery/organ1.ogg | Bin 0 -> 18912 bytes
Resources/Audio/Medical/Surgery/organ2.ogg | Bin 0 -> 18946 bytes
.../Audio/Medical/Surgery/retractor1.ogg | Bin 0 -> 11537 bytes
.../Audio/Medical/Surgery/retractor2.ogg | Bin 0 -> 9915 bytes
Resources/Audio/Medical/Surgery/saw.ogg | Bin 0 -> 46750 bytes
Resources/Audio/Medical/Surgery/scalpel1.ogg | Bin 0 -> 13736 bytes
Resources/Audio/Medical/Surgery/scalpel2.ogg | Bin 0 -> 13098 bytes
.../en-US/escape-menu/ui/options-menu.ftl | 8 +
Resources/Locale/en-US/guidebook/guides.ftl | 4 +
.../components/health-analyzer-component.ftl | 1 +
Resources/Locale/en-US/mood/mood.ftl | 2 +
Resources/Locale/en-US/surgery/surgery-ui.ftl | 12 +
Resources/Prototypes/Body/Organs/felinid.yml | 24 +
Resources/Prototypes/Body/Organs/human.yml | 15 +
Resources/Prototypes/Body/Parts/animal.yml | 2 +-
Resources/Prototypes/Body/Parts/base.yml | 55 +-
Resources/Prototypes/Body/Parts/harpy.yml | 51 +-
Resources/Prototypes/Body/Parts/shadowkin.yml | 2 +-
Resources/Prototypes/Body/Parts/skeleton.yml | 31 +-
Resources/Prototypes/Body/Parts/vox.yml | 2 +-
.../Prototypes/Body/Prototypes/a_ghost.yml | 16 +-
.../Prototypes/Body/Prototypes/human.yml | 32 +-
.../Catalog/Fills/Backpacks/duffelbag.yml | 2 +
.../Catalog/Fills/Crates/medical.yml | 4 +-
.../Catalog/Fills/Lockers/heads.yml | 2 +
Resources/Prototypes/Damage/containers.yml | 8 +
Resources/Prototypes/Damage/modifier_sets.yml | 11 +
.../DeltaV/Roles/Jobs/Security/brigmedic.yml | 2 +
.../Entities/Clothing/Belt/belts.yml | 2 +-
.../Entities/Debugging/debug_sweps.yml | 48 ++
.../Prototypes/Entities/Mobs/NPCs/animals.yml | 8 +-
.../Entities/Mobs/Player/silicon_base.yml | 6 +-
.../Prototypes/Entities/Mobs/Species/base.yml | 4 +
.../Entities/Mobs/Species/skeleton.yml | 3 +-
.../Entities/Mobs/Species/slime.yml | 2 +
.../Circuitboards/Machine/production.yml | 14 +
.../Objects/Specific/Medical/surgery.yml | 188 ++++-
.../Furniture/Tables/operating_table.yml | 3 +-
.../Entities/Structures/Machines/lathe.yml | 48 ++
.../Prototypes/Entities/Surgery/surgeries.yml | 539 +++++++++++++
.../Entities/Surgery/surgery_steps.yml | 563 +++++++++++++
.../Prototypes/EntityLists/Tools/surgery.yml | 8 +-
Resources/Prototypes/Guidebook/medical.yml | 29 +
Resources/Prototypes/Mood/genericNeeds.yml | 2 +-
.../Mood/genericNegativeEffects.yml | 6 +
Resources/Prototypes/Reagents/gases.yml | 16 +-
.../Prototypes/Recipes/Lathes/medical.yml | 8 +
.../Recipes/Lathes/rehydrateable.yml | 98 +++
.../Prototypes/Roles/Jobs/Medical/chemist.yml | 2 +
.../Jobs/Medical/chief_medical_officer.yml | 2 +
.../Roles/Jobs/Medical/medical_doctor.yml | 2 +
.../Roles/Jobs/Medical/medical_intern.yml | 2 +
.../Roles/Jobs/Medical/paramedic.yml | 2 +
.../Roles/Jobs/Medical/senior_physician.yml | 2 +
Resources/Prototypes/Species/misc.yml | 12 +
.../Guidebook/Medical/OrganManipulation.xml | 51 ++
.../Guidebook/Medical/PartManipulation.xml | 51 ++
.../ServerInfo/Guidebook/Medical/Surgery.xml | 40 +
.../Guidebook/Medical/UtilitySurgeries.xml | 24 +
.../Textures/Interface/Ashen/target_doll.png | Bin 0 -> 422 bytes
.../Interface/Clockwork/target_doll.png | Bin 0 -> 1219 bytes
.../Interface/Default/target_doll.png | Bin 0 -> 422 bytes
.../Interface/Minimalist/target_doll.png | Bin 0 -> 422 bytes
.../Interface/Plasmafire/target_doll.png | Bin 0 -> 462 bytes
.../Textures/Interface/Retro/target_doll.png | Bin 0 -> 383 bytes
.../Interface/Slimecore/target_doll.png | Bin 0 -> 422 bytes
.../Interface/Targeting/Doll/eyes.png | Bin 0 -> 5227 bytes
.../Interface/Targeting/Doll/eyes_hover.png | Bin 0 -> 6856 bytes
.../Interface/Targeting/Doll/groin.png | Bin 0 -> 5485 bytes
.../Interface/Targeting/Doll/groin_hover.png | Bin 0 -> 6452 bytes
.../Interface/Targeting/Doll/head.png | Bin 0 -> 5663 bytes
.../Interface/Targeting/Doll/head_hover.png | Bin 0 -> 7153 bytes
.../Interface/Targeting/Doll/leftarm.png | Bin 0 -> 5779 bytes
.../Targeting/Doll/leftarm_hover.png | Bin 0 -> 6652 bytes
.../Interface/Targeting/Doll/leftfoot.png | Bin 0 -> 5462 bytes
.../Targeting/Doll/leftfoot_hover.png | Bin 0 -> 5476 bytes
.../Interface/Targeting/Doll/lefthand.png | Bin 0 -> 5462 bytes
.../Targeting/Doll/lefthand_hover.png | Bin 0 -> 6174 bytes
.../Interface/Targeting/Doll/leftleg.png | Bin 0 -> 5488 bytes
.../Targeting/Doll/leftleg_hover.png | Bin 0 -> 6185 bytes
.../Interface/Targeting/Doll/mouth.png | Bin 0 -> 5219 bytes
.../Interface/Targeting/Doll/mouth_hover.png | Bin 0 -> 5208 bytes
.../Interface/Targeting/Doll/rightarm.png | Bin 0 -> 5773 bytes
.../Targeting/Doll/rightarm_hover.png | Bin 0 -> 6592 bytes
.../Interface/Targeting/Doll/rightfoot.png | Bin 0 -> 5454 bytes
.../Targeting/Doll/rightfoot_hover.png | Bin 0 -> 5465 bytes
.../Interface/Targeting/Doll/righthand.png | Bin 0 -> 5467 bytes
.../Targeting/Doll/righthand_hover.png | Bin 0 -> 6207 bytes
.../Interface/Targeting/Doll/rightleg.png | Bin 0 -> 5473 bytes
.../Targeting/Doll/rightleg_hover.png | Bin 0 -> 5908 bytes
.../Interface/Targeting/Doll/torso.png | Bin 0 -> 5284 bytes
.../Interface/Targeting/Doll/torso_hover.png | Bin 0 -> 5672 bytes
.../Targeting/Status/groin.rsi/groin_0.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_1.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_2.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_3.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_4.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_5.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_6.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_7.png | Bin 0 -> 157 bytes
.../Targeting/Status/groin.rsi/groin_8.png | Bin 0 -> 178 bytes
.../Targeting/Status/groin.rsi/meta.json | 38 +
.../Targeting/Status/head.rsi/head_0.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_1.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_2.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_3.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_4.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_5.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_6.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_7.png | Bin 0 -> 140 bytes
.../Targeting/Status/head.rsi/head_8.png | Bin 0 -> 155 bytes
.../Targeting/Status/head.rsi/meta.json | 38 +
.../Status/leftarm.rsi/leftarm_0.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_1.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_2.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_3.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_4.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_5.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_6.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_7.png | Bin 0 -> 144 bytes
.../Status/leftarm.rsi/leftarm_8.png | Bin 0 -> 154 bytes
.../Targeting/Status/leftarm.rsi/meta.json | 38 +
.../Status/leftfoot.rsi/leftfoot_0.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_1.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_2.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_3.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_4.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_5.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_6.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_7.png | Bin 0 -> 145 bytes
.../Status/leftfoot.rsi/leftfoot_8.png | Bin 0 -> 166 bytes
.../Targeting/Status/leftfoot.rsi/meta.json | 38 +
.../Status/lefthand.rsi/lefthand_0.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_1.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_2.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_3.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_4.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_5.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_6.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_7.png | Bin 0 -> 136 bytes
.../Status/lefthand.rsi/lefthand_8.png | Bin 0 -> 152 bytes
.../Targeting/Status/lefthand.rsi/meta.json | 38 +
.../Status/leftleg.rsi/leftleg_0.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_1.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_2.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_3.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_4.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_5.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_6.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_7.png | Bin 0 -> 141 bytes
.../Status/leftleg.rsi/leftleg_8.png | Bin 0 -> 170 bytes
.../Targeting/Status/leftleg.rsi/meta.json | 38 +
.../Targeting/Status/rightarm.rsi/meta.json | 38 +
.../Status/rightarm.rsi/rightarm_0.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_1.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_2.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_3.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_4.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_5.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_6.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_7.png | Bin 0 -> 147 bytes
.../Status/rightarm.rsi/rightarm_8.png | Bin 0 -> 158 bytes
.../Targeting/Status/rightfoot.rsi/meta.json | 38 +
.../Status/rightfoot.rsi/rightfoot_0.png | Bin 0 -> 176 bytes
.../Status/rightfoot.rsi/rightfoot_1.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_2.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_3.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_4.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_5.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_6.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_7.png | Bin 0 -> 144 bytes
.../Status/rightfoot.rsi/rightfoot_8.png | Bin 0 -> 161 bytes
.../Targeting/Status/righthand.rsi/meta.json | 38 +
.../Status/righthand.rsi/righthand_0.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_1.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_2.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_3.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_4.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_5.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_6.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_7.png | Bin 0 -> 138 bytes
.../Status/righthand.rsi/righthand_8.png | Bin 0 -> 152 bytes
.../Targeting/Status/rightleg.rsi/meta.json | 38 +
.../Status/rightleg.rsi/rightleg_0.png | Bin 0 -> 178 bytes
.../Status/rightleg.rsi/rightleg_1.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_2.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_3.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_4.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_5.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_6.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_7.png | Bin 0 -> 141 bytes
.../Status/rightleg.rsi/rightleg_8.png | Bin 0 -> 166 bytes
.../Targeting/Status/torso.rsi/meta.json | 38 +
.../Targeting/Status/torso.rsi/torso_0.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_1.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_2.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_3.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_4.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_5.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_6.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_7.png | Bin 0 -> 160 bytes
.../Targeting/Status/torso.rsi/torso_8.png | Bin 0 -> 186 bytes
.../Species/Misc/Pizza/parts.rsi/l_arm.png | Bin 0 -> 572 bytes
.../Species/Misc/Pizza/parts.rsi/meta.json | 19 +
.../Species/Misc/Pizza/parts.rsi/r_arm.png | Bin 0 -> 608 bytes
.../Medical/Surgery/bone_gel.rsi/bone-gel.png | Bin 0 -> 432 bytes
.../Surgery/bone_gel.rsi/bone-gel_0.png | Bin 0 -> 391 bytes
.../Surgery/bone_gel.rsi/bone-gel_25.png | Bin 0 -> 444 bytes
.../Surgery/bone_gel.rsi/bone-gel_50.png | Bin 0 -> 456 bytes
.../Surgery/bone_gel.rsi/bone-gel_75.png | Bin 0 -> 444 bytes
.../Medical/Surgery/bone_gel.rsi/meta.json | 29 +
.../bone_gel.rsi/predator_bone-gel.png | Bin 0 -> 585 bytes
.../Surgery/bonesetter.rsi/bonesetter.png | Bin 0 -> 581 bytes
.../Medical/Surgery/bonesetter.rsi/meta.json | 17 +
.../bonesetter.rsi/predator_bonesetter.png | Bin 0 -> 489 bytes
.../Surgery/manipulation.rsi/insertion.png | Bin 0 -> 379 bytes
.../Surgery/manipulation.rsi/meta.json | 14 +
.../limbgrower.rsi/limbgrower_fill.png | Bin 0 -> 9056 bytes
.../limbgrower.rsi/limbgrower_idleoff.png | Bin 0 -> 3320 bytes
.../limbgrower.rsi/limbgrower_idleon.png | Bin 0 -> 11102 bytes
.../limbgrower.rsi/limbgrower_openpanel.png | Bin 0 -> 3981 bytes
.../limbgrower.rsi/limbgrower_panelopen.png | Bin 0 -> 2273 bytes
.../limbgrower.rsi/limbgrower_unfill.png | Bin 0 -> 9104 bytes
.../Machines/limbgrower.rsi/meta.json | 85 ++
Resources/keybinds.yml | 25 +
384 files changed, 8214 insertions(+), 199 deletions(-)
create mode 100644 Content.Client/Body/Components/BrainComponent.cs
create mode 100644 Content.Client/Body/Components/LungComponent.cs
create mode 100644 Content.Client/Body/Components/StomachComponent.cs
create mode 100644 Content.Client/Medical/Surgery/SurgeryBui.cs
create mode 100644 Content.Client/Medical/Surgery/SurgeryStepButton.xaml
create mode 100644 Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs
create mode 100644 Content.Client/Medical/Surgery/SurgerySystem.cs
create mode 100644 Content.Client/Medical/Surgery/SurgeryWindow.xaml
create mode 100644 Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs
create mode 100644 Content.Client/Targeting/TargetingSystem.cs
create mode 100644 Content.Client/UserInterface/Systems/PartStatus/PartStatusUIController.cs
create mode 100644 Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml
create mode 100644 Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs
create mode 100644 Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs
create mode 100644 Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml
create mode 100644 Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs
create mode 100644 Content.Client/Xenonids/UI/XenoChoiceControl.xaml
create mode 100644 Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs
create mode 100644 Content.IntegrationTests/Tests/Shitmed/Body/SpeciesBUiTest.cs
create mode 100644 Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs
create mode 100644 Content.Server/Medical/Surgery/SurgerySystem.cs
create mode 100644 Content.Server/Targeting/TargetingSystem.cs
create mode 100644 Content.Shared/Body/Events/AmputateAttemptEvent.cs
create mode 100644 Content.Shared/Body/Organ/DebrainedComponent.cs
create mode 100644 Content.Shared/Body/Organ/EarsComponent.cs
create mode 100644 Content.Shared/Body/Organ/EyesComponent.cs
create mode 100644 Content.Shared/Body/Organ/HeartComponent.cs
create mode 100644 Content.Shared/Body/Organ/LiverComponent.cs
create mode 100644 Content.Shared/Body/Organ/MarkingContainerComponent.cs
create mode 100644 Content.Shared/Body/Organ/TailComponent.cs
create mode 100644 Content.Shared/Body/Part/BodyPartAppearanceComponent.cs
create mode 100644 Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs
create mode 100644 Content.Shared/Body/Systems/SharedBodySystem.Targeting.cs
create mode 100644 Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryLarvaConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryMarkingConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryOrganConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryPartConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryPartPresentCondition.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryValidEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Conditions/SurgeryWoundedConditionComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Complete/SurgeryCompletedEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs
create mode 100644 Content.Shared/Medical/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/OperatingTableComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs
create mode 100644 Content.Shared/Medical/Surgery/SharedSurgerySystem.cs
create mode 100644 Content.Shared/Medical/Surgery/StepInvalidReason.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/BleedersClampedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/BodyPartReattachedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/BodyPartSawedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/IncisionOpenComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/OrganReattachedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/PartRemovedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/RibcageOpenComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/RibcageSawedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/Parts/SkinRetractedComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryAddMarkingStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryAddOrganStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryAddPartStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryAffixOrganStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryAffixPartStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryCanPerformStepEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryRemoveMarkingStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryRemovePartStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryRepeatableStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryDoAfterEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgerySpeedModifierComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryStepDamageEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryStepEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryUI.cs
create mode 100644 Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/BoneGelComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/BoneSawComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/BoneSetterComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/CauteryComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/HemostatComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/ISurgeryToolComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/RetractorComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/ScalpelComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/SurgeryToolComponent.cs
create mode 100644 Content.Shared/Medical/Surgery/Tools/SurgicalDrillComponent.cs
create mode 100644 Content.Shared/Targeting/Events.cs
create mode 100644 Content.Shared/Targeting/SharedTargetingSystem.cs
create mode 100644 Content.Shared/Targeting/TargetBodyPart.cs
create mode 100644 Content.Shared/Targeting/TargetIntegrity.cs
create mode 100644 Content.Shared/Targeting/TargetingComponent.cs
create mode 100644 Resources/Audio/Medical/Surgery/attributions.yml
create mode 100644 Resources/Audio/Medical/Surgery/cautery1.ogg
create mode 100644 Resources/Audio/Medical/Surgery/cautery2.ogg
create mode 100644 Resources/Audio/Medical/Surgery/hemostat1.ogg
create mode 100644 Resources/Audio/Medical/Surgery/organ1.ogg
create mode 100644 Resources/Audio/Medical/Surgery/organ2.ogg
create mode 100644 Resources/Audio/Medical/Surgery/retractor1.ogg
create mode 100644 Resources/Audio/Medical/Surgery/retractor2.ogg
create mode 100644 Resources/Audio/Medical/Surgery/saw.ogg
create mode 100644 Resources/Audio/Medical/Surgery/scalpel1.ogg
create mode 100644 Resources/Audio/Medical/Surgery/scalpel2.ogg
create mode 100644 Resources/Locale/en-US/surgery/surgery-ui.ftl
create mode 100644 Resources/Prototypes/Body/Organs/felinid.yml
create mode 100644 Resources/Prototypes/Entities/Surgery/surgeries.yml
create mode 100644 Resources/Prototypes/Entities/Surgery/surgery_steps.yml
create mode 100644 Resources/Prototypes/Species/misc.yml
create mode 100644 Resources/ServerInfo/Guidebook/Medical/OrganManipulation.xml
create mode 100644 Resources/ServerInfo/Guidebook/Medical/PartManipulation.xml
create mode 100644 Resources/ServerInfo/Guidebook/Medical/Surgery.xml
create mode 100644 Resources/ServerInfo/Guidebook/Medical/UtilitySurgeries.xml
create mode 100644 Resources/Textures/Interface/Ashen/target_doll.png
create mode 100644 Resources/Textures/Interface/Clockwork/target_doll.png
create mode 100644 Resources/Textures/Interface/Default/target_doll.png
create mode 100644 Resources/Textures/Interface/Minimalist/target_doll.png
create mode 100644 Resources/Textures/Interface/Plasmafire/target_doll.png
create mode 100644 Resources/Textures/Interface/Retro/target_doll.png
create mode 100644 Resources/Textures/Interface/Slimecore/target_doll.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/eyes.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/eyes_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/groin.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/groin_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/head.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/head_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftarm.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftarm_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftfoot.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftfoot_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/lefthand.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/lefthand_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftleg.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/leftleg_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/mouth.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/mouth_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightarm.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightarm_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightfoot.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightfoot_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/righthand.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/righthand_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightleg.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/rightleg_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/torso.png
create mode 100644 Resources/Textures/Interface/Targeting/Doll/torso_hover.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/groin_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/groin.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/head_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/head.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/leftarm_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftarm.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/leftfoot_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftfoot.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/lefthand_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/lefthand.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/leftleg_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/leftleg.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightarm.rsi/rightarm_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightfoot.rsi/rightfoot_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/righthand.rsi/righthand_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/rightleg.rsi/rightleg_8.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/meta.json
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_0.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_1.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_2.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_3.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_4.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_5.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_6.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_7.png
create mode 100644 Resources/Textures/Interface/Targeting/Status/torso.rsi/torso_8.png
create mode 100644 Resources/Textures/Mobs/Species/Misc/Pizza/parts.rsi/l_arm.png
create mode 100644 Resources/Textures/Mobs/Species/Misc/Pizza/parts.rsi/meta.json
create mode 100644 Resources/Textures/Mobs/Species/Misc/Pizza/parts.rsi/r_arm.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_0.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_25.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_50.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/bone-gel_75.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/meta.json
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bone_gel.rsi/predator_bone-gel.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/bonesetter.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/meta.json
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/bonesetter.rsi/predator_bonesetter.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/insertion.png
create mode 100644 Resources/Textures/Objects/Specific/Medical/Surgery/manipulation.rsi/meta.json
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_fill.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleoff.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_idleon.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_openpanel.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_panelopen.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/limbgrower_unfill.png
create mode 100644 Resources/Textures/Structures/Machines/limbgrower.rsi/meta.json
diff --git a/Content.Client/Body/Components/BrainComponent.cs b/Content.Client/Body/Components/BrainComponent.cs
new file mode 100644
index 00000000000..5ef9cea9901
--- /dev/null
+++ b/Content.Client/Body/Components/BrainComponent.cs
@@ -0,0 +1,3 @@
+namespace Content.Client.Body.Components;
+[RegisterComponent]
+public sealed partial class BrainComponent : Component { }
diff --git a/Content.Client/Body/Components/LungComponent.cs b/Content.Client/Body/Components/LungComponent.cs
new file mode 100644
index 00000000000..71a19323b87
--- /dev/null
+++ b/Content.Client/Body/Components/LungComponent.cs
@@ -0,0 +1,3 @@
+namespace Content.Client.Body.Components;
+[RegisterComponent]
+public sealed partial class LungComponent : Component { }
diff --git a/Content.Client/Body/Components/StomachComponent.cs b/Content.Client/Body/Components/StomachComponent.cs
new file mode 100644
index 00000000000..fbc06ac7d75
--- /dev/null
+++ b/Content.Client/Body/Components/StomachComponent.cs
@@ -0,0 +1,3 @@
+namespace Content.Client.Body.Components;
+[RegisterComponent]
+public sealed partial class StomachComponent : Component { }
diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs
index bab785525b0..10dc057a8fd 100644
--- a/Content.Client/Body/Systems/BodySystem.cs
+++ b/Content.Client/Body/Systems/BodySystem.cs
@@ -1,7 +1,72 @@
using Content.Shared.Body.Systems;
+using Content.Shared.Body.Part;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
+using Content.Shared.Body.Components;
namespace Content.Client.Body.Systems;
public sealed class BodySystem : SharedBodySystem
{
+ [Dependency] private readonly MarkingManager _markingManager = default!;
+
+ private void ApplyMarkingToPart(MarkingPrototype markingPrototype,
+ IReadOnlyList? colors,
+ bool visible,
+ SpriteComponent sprite)
+ {
+ for (var j = 0; j < markingPrototype.Sprites.Count; j++)
+ {
+ var markingSprite = markingPrototype.Sprites[j];
+
+ if (markingSprite is not SpriteSpecifier.Rsi rsi)
+ continue;
+
+ var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
+
+ if (!sprite.LayerMapTryGet(layerId, out _))
+ {
+ var layer = sprite.AddLayer(markingSprite, j + 1);
+ sprite.LayerMapSet(layerId, layer);
+ sprite.LayerSetSprite(layerId, rsi);
+ }
+
+ sprite.LayerSetVisible(layerId, visible);
+
+ if (!visible)
+ continue;
+
+ // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
+ // and we need to check the index is correct. So if that happens just default to white?
+ if (colors != null && j < colors.Count)
+ sprite.LayerSetColor(layerId, colors[j]);
+ else
+ sprite.LayerSetColor(layerId, Color.White);
+ }
+ }
+
+ protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component)
+ {
+ if (!TryComp(target, out SpriteComponent? sprite))
+ return;
+
+ if (component.Color != null)
+ sprite.Color = component.Color.Value;
+
+ foreach (var (visualLayer, markingList) in component.Markings)
+ foreach (var marking in markingList)
+ {
+ if (!_markingManager.TryGetMarking(marking, out var markingPrototype))
+ continue;
+
+ ApplyMarkingToPart(markingPrototype, marking.MarkingColors, marking.Visible, sprite);
+ }
+ }
+
+ protected override void RemoveBodyMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance)
+ {
+ return;
+ }
}
diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs
index 7319b97b42b..7ea3b69de57 100644
--- a/Content.Client/Hands/Systems/HandsSystem.cs
+++ b/Content.Client/Hands/Systems/HandsSystem.cs
@@ -3,6 +3,7 @@
using Content.Client.Examine;
using Content.Client.Strip;
using Content.Client.Verbs.UI;
+using Content.Shared.Body.Part;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
@@ -38,7 +39,6 @@ public sealed class HandsSystem : SharedHandsSystem
public event Action? OnPlayerItemRemoved;
public event Action? OnPlayerHandBlocked;
public event Action? OnPlayerHandUnblocked;
-
public override void Initialize()
{
base.Initialize();
@@ -49,6 +49,8 @@ public override void Initialize()
SubscribeLocalEvent(OnHandsShutdown);
SubscribeLocalEvent(HandleComponentState);
SubscribeLocalEvent(OnVisualsChanged);
+ SubscribeLocalEvent(HandleBodyPartRemoved);
+ SubscribeLocalEvent(HandleBodyPartDisabled);
OnHandSetActive += OnHandActivated;
}
@@ -236,8 +238,38 @@ public void UIHandAltActivateItem(string handName)
RaisePredictiveEvent(new RequestHandAltInteractEvent(handName));
}
+ #region pulling
+
+ #endregion
+
#region visuals
+ private void HideLayers(EntityUid uid, HandsComponent component, Entity part, SpriteComponent? sprite = null)
+ {
+ if (part.Comp.PartType != BodyPartType.Hand || !Resolve(uid, ref sprite, logMissing: false))
+ return;
+
+ var location = part.Comp.Symmetry switch
+ {
+ BodyPartSymmetry.None => HandLocation.Middle,
+ BodyPartSymmetry.Left => HandLocation.Left,
+ BodyPartSymmetry.Right => HandLocation.Right,
+ _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry))
+ };
+
+ if (component.RevealedLayers.TryGetValue(location, out var revealedLayers))
+ {
+ foreach (var key in revealedLayers)
+ sprite.RemoveLayer(key);
+
+ revealedLayers.Clear();
+ }
+ }
+
+ private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) => HideLayers(uid, component, args.Part);
+
+ private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args) => HideLayers(uid, component, args.Part);
+
protected override void HandleEntityInserted(EntityUid uid, HandsComponent hands, EntInsertedIntoContainerMessage args)
{
base.HandleEntityInserted(uid, hands, args);
@@ -262,6 +294,7 @@ protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands,
if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
return;
+
UpdateHandVisuals(uid, args.Entity, hand);
_stripSys.UpdateUi(uid);
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
index dc0a3e9fccd..39bb52d72c0 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs
@@ -1,4 +1,5 @@
using Content.Shared.MedicalScanner;
+using Content.Shared.Targeting;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -22,6 +23,7 @@ protected override void Open()
Title = EntMan.GetComponent(Owner).EntityName,
};
_window.OnClose += Close;
+ _window.OnBodyPartSelected += SendBodyPartMessage;
_window.OpenCentered();
}
@@ -36,6 +38,8 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
_window.Populate(cast);
}
+ private void SendBodyPartMessage(TargetBodyPart? part, EntityUid target) => SendMessage(new HealthAnalyzerPartMessage(EntMan.GetNetEntity(target), part ?? null));
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -43,7 +47,10 @@ protected override void Dispose(bool disposing)
return;
if (_window != null)
+ {
_window.OnClose -= Close;
+ _window.OnBodyPartSelected -= SendBodyPartMessage;
+ }
_window?.Dispose();
}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index e070af95d82..0a0b5ac89e7 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -2,7 +2,7 @@
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MaxHeight="525"
- MinWidth="300">
+ MinWidth="350">
-
+ Text="{Loc health-analyzer-window-no-patient-data-text}"/>
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
? OnBodyPartSelected;
+ private EntityUid _spriteViewEntity;
+
+ [ValidatePrototypeId]
+ private readonly EntProtoId _bodyView = "AlertSpriteView";
+
+ private readonly Dictionary _bodyPartControls;
+ private EntityUid? _target;
+
+ // End-Shitmed
+
public HealthAnalyzerWindow()
{
RobustXamlLoader.Load(this);
@@ -45,19 +58,78 @@ public HealthAnalyzerWindow()
_spriteSystem = _entityManager.System();
_prototypes = dependencies.Resolve();
_cache = dependencies.Resolve();
+ // Start-Shitmed
+ _bodyPartControls = new Dictionary
+ {
+ { TargetBodyPart.Head, HeadButton },
+ { TargetBodyPart.Torso, ChestButton },
+ { TargetBodyPart.Groin, GroinButton },
+ { TargetBodyPart.LeftArm, LeftArmButton },
+ { TargetBodyPart.LeftHand, LeftHandButton },
+ { TargetBodyPart.RightArm, RightArmButton },
+ { TargetBodyPart.RightHand, RightHandButton },
+ { TargetBodyPart.LeftLeg, LeftLegButton },
+ { TargetBodyPart.LeftFoot, LeftFootButton },
+ { TargetBodyPart.RightLeg, RightLegButton },
+ { TargetBodyPart.RightFoot, RightFootButton },
+ };
+
+ foreach (var bodyPartButton in _bodyPartControls)
+ {
+ bodyPartButton.Value.MouseFilter = MouseFilterMode.Stop;
+ bodyPartButton.Value.OnPressed += _ => SetActiveBodyPart(bodyPartButton.Key, bodyPartButton.Value);
+ }
+ ReturnButton.OnPressed += _ => ResetBodyPart();
+ // End-Shitmed
+ }
+
+ public void SetActiveBodyPart(TargetBodyPart part, TextureButton button)
+ {
+ if (_target == null)
+ return;
+
+ // Bit of the ole shitcode until we have Groins in the prototypes.
+ OnBodyPartSelected?.Invoke(part == TargetBodyPart.Groin ? TargetBodyPart.Torso : part, _target.Value);
+ }
+
+ public void ResetBodyPart()
+ {
+ if (_target == null)
+ return;
+
+ OnBodyPartSelected?.Invoke(null, _target.Value);
+ }
+
+ public void SetActiveButtons(bool isHumanoid)
+ {
+ foreach (var button in _bodyPartControls)
+ button.Value.Visible = isHumanoid;
}
public void Populate(HealthAnalyzerScannedUserMessage msg)
{
- var target = _entityManager.GetEntity(msg.TargetEntity);
+ // Start-Shitmed
+ _target = _entityManager.GetEntity(msg.TargetEntity);
+ EntityUid? part = msg.Part != null ? _entityManager.GetEntity(msg.Part.Value) : null;
+ var isPart = part != null;
- if (target == null
- || !_entityManager.TryGetComponent(target, out var damageable))
+ if (_target == null
+ || !_entityManager.TryGetComponent(isPart ? part : _target, out var damageable))
{
NoPatientDataText.Visible = true;
return;
}
+ SetActiveButtons(_entityManager.HasComponent(_target.Value));
+
+ ReturnButton.Visible = isPart;
+ PartNameLabel.Visible = isPart;
+
+ if (part != null)
+ PartNameLabel.Text = _entityManager.HasComponent(part)
+ ? Identity.Name(part.Value, _entityManager)
+ : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
+
NoPatientDataText.Visible = false;
// Scan Mode
@@ -72,19 +144,20 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
// Patient Information
- SpriteView.SetEntity(target.Value);
+ SpriteView.SetEntity(SetupIcon(msg.Body) ?? _target.Value);
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
+ PartView.Visible = SpriteView.Visible;
NoDataTex.Visible = !SpriteView.Visible;
var name = new FormattedMessage();
name.PushColor(Color.White);
- name.AddText(_entityManager.HasComponent(target.Value)
- ? Identity.Name(target.Value, _entityManager)
+ name.AddText(_entityManager.HasComponent(_target.Value)
+ ? Identity.Name(_target.Value, _entityManager)
: Loc.GetString("health-analyzer-window-entity-unknown-text"));
NameLabel.SetMessage(name);
SpeciesLabel.Text =
- _entityManager.TryGetComponent(target.Value,
+ _entityManager.TryGetComponent(_target.Value,
out var humanoidAppearanceComponent)
? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name)
: Loc.GetString("health-analyzer-window-entity-unknown-species-text");
@@ -100,7 +173,7 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
: Loc.GetString("health-analyzer-window-entity-unknown-value-text");
StatusLabel.Text =
- _entityManager.TryGetComponent(target.Value, out var mobStateComponent)
+ _entityManager.TryGetComponent(_target.Value, out var mobStateComponent)
? GetStatus(mobStateComponent.CurrentState)
: Loc.GetString("health-analyzer-window-entity-unknown-text");
@@ -149,6 +222,7 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
IReadOnlyDictionary damagePerType = damageable.Damage.DamageDict;
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
+ // End-Shitmed
}
private static string GetStatus(MobState mobState)
@@ -249,5 +323,41 @@ private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
return rootContainer;
}
+
+ // Start-Shitmed
+ ///
+ /// Sets up the Body Doll using Alert Entity to use in Health Analyzer.
+ ///
+ private EntityUid? SetupIcon(Dictionary? body)
+ {
+ if (body is null)
+ return null;
+
+ if (!_entityManager.Deleted(_spriteViewEntity))
+ _entityManager.QueueDeleteEntity(_spriteViewEntity);
+
+ _spriteViewEntity = _entityManager.Spawn(_bodyView);
+
+ if (!_entityManager.TryGetComponent(_spriteViewEntity, out var sprite))
+ return null;
+
+ int layer = 0;
+ foreach (var (bodyPart, integrity) in body)
+ {
+ // TODO: Fix this way PartStatusUIController and make it use layers instead of TextureRects
+ string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown";
+ int enumValue = (int) integrity;
+ var rsi = new SpriteSpecifier.Rsi(new ResPath($"/Textures/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}");
+ // It's probably shitcode but im lazy to get into sprite stuff - It is shitcode :)
+ if (!sprite.TryGetLayer(layer, out _))
+ sprite.AddLayer(_spriteSystem.Frame0(rsi));
+ else
+ sprite.LayerSetTexture(layer, _spriteSystem.Frame0(rsi));
+ sprite.LayerSetScale(layer, new Vector2(3f, 3f));
+ layer++;
+ }
+ return _spriteViewEntity;
+ }
+ // End-Shitmed
}
}
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 867dcbc2692..b05a16b6d4f 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -65,7 +65,8 @@ private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent
foreach (var (key, info) in component.CustomBaseLayers)
{
oldLayers.Remove(key);
- SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color);
+ // Shitmed modification: For whatever reason these weren't actually ignoring the skin color as advertised.
+ SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color, overrideSkin: true);
}
// hide old layers
@@ -83,7 +84,8 @@ private void SetLayerData(
HumanoidVisualLayers key,
string? protoId,
bool sexMorph = false,
- Color? color = null)
+ Color? color = null,
+ bool overrideSkin = false)
{
var layerIndex = sprite.LayerMapReserveBlank(key);
var layer = sprite[layerIndex];
@@ -101,7 +103,7 @@ private void SetLayerData(
var proto = _prototypeManager.Index(protoId);
component.BaseLayers[key] = proto;
- if (proto.MatchSkin)
+ if (proto.MatchSkin && !overrideSkin)
layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha);
if (proto.BaseSprite != null)
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 2b1361208d1..188d664b395 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -86,6 +86,12 @@ public static void SetupContexts(IInputContextContainer contexts)
human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3);
human.AddFunction(ContentKeyFunctions.LookUp);
+ human.AddFunction(ContentKeyFunctions.TargetHead);
+ human.AddFunction(ContentKeyFunctions.TargetTorso);
+ human.AddFunction(ContentKeyFunctions.TargetLeftArm);
+ human.AddFunction(ContentKeyFunctions.TargetRightArm);
+ human.AddFunction(ContentKeyFunctions.TargetLeftLeg);
+ human.AddFunction(ContentKeyFunctions.TargetRightLeg);
// actions should be common (for ghosts, mobs, etc)
common.AddFunction(ContentKeyFunctions.OpenActionsMenu);
diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs
index 87cea4e3d2f..053d1240578 100644
--- a/Content.Client/Inventory/ClientInventorySystem.cs
+++ b/Content.Client/Inventory/ClientInventorySystem.cs
@@ -5,6 +5,7 @@
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Storage;
+using Content.Shared.Targeting.Events;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Client.UserInterface;
@@ -39,7 +40,7 @@ public override void Initialize()
SubscribeLocalEvent(OnPlayerAttached);
SubscribeLocalEvent(OnPlayerDetached);
-
+ SubscribeLocalEvent(OnRefreshInventorySlots);
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent((_, comp, args) =>
@@ -181,6 +182,15 @@ public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, strin
EntitySlotUpdate?.Invoke(newData);
}
+ public void OnRefreshInventorySlots(EntityUid owner, InventorySlotsComponent component, RefreshInventorySlotsEvent args)
+ {
+ if (!component.SlotData.TryGetValue(args.SlotName, out var slotData)
+ || _playerManager.LocalEntity != owner)
+ return;
+
+ OnSlotRemoved?.Invoke(slotData);
+ }
+
public bool TryAddSlotDef(EntityUid owner, InventorySlotsComponent component, SlotDefinition newSlotDef)
{
SlotData newSlotData = newSlotDef; //convert to slotData
@@ -237,6 +247,7 @@ public void UIInventoryAltActivateItem(string slot, EntityUid uid)
public sealed class SlotData
{
+ [ViewVariables]
public readonly SlotDefinition SlotDef;
public EntityUid? HeldEntity => Container?.ContainedEntity;
public bool Blocked;
diff --git a/Content.Client/Inventory/InventorySlotsComponent.cs b/Content.Client/Inventory/InventorySlotsComponent.cs
index d4667bcef33..84bc7d54392 100644
--- a/Content.Client/Inventory/InventorySlotsComponent.cs
+++ b/Content.Client/Inventory/InventorySlotsComponent.cs
@@ -8,7 +8,7 @@ namespace Content.Client.Inventory;
public sealed partial class InventorySlotsComponent : Component
{
[ViewVariables]
- public readonly Dictionary SlotData = new ();
+ public readonly Dictionary SlotData = new();
///
/// Data about the current layers that have been added to the players sprite due to the items in each equipment slot.
diff --git a/Content.Client/Medical/Surgery/SurgeryBui.cs b/Content.Client/Medical/Surgery/SurgeryBui.cs
new file mode 100644
index 00000000000..a49d5ec06b3
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryBui.cs
@@ -0,0 +1,358 @@
+using Content.Client.Xenonids.UI;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Shared.Medical.Surgery;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Medical.Surgery;
+
+[UsedImplicitly]
+public sealed class SurgeryBui : BoundUserInterface
+{
+ [Dependency] private readonly IEntityManager _entities = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ private readonly SurgerySystem _system;
+ [ViewVariables]
+ private SurgeryWindow? _window;
+ private EntityUid? _part;
+ private bool _isBody;
+ private (EntityUid Ent, EntProtoId Proto)? _surgery;
+ private readonly List _previousSurgeries = new();
+ public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) => _system = _entities.System();
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ if (_window is null
+ || message is not SurgeryBuiRefreshMessage)
+ return;
+
+ RefreshUI();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not SurgeryBuiState s)
+ return;
+
+ Update(s);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ _window?.Dispose();
+ }
+
+ private void Update(SurgeryBuiState state)
+ {
+ if (!_entities.TryGetComponent(_player.LocalEntity, out SurgeryTargetComponent? surgeryTargetComp)
+ || !surgeryTargetComp.CanOperate)
+ return;
+
+ if (_window == null)
+ {
+ _window = new SurgeryWindow();
+ _window.OnClose += Close;
+ _window.Title = Loc.GetString("surgery-ui-window-title");
+
+ _window.PartsButton.OnPressed += _ =>
+ {
+ _part = null;
+ _isBody = false;
+ _surgery = null;
+ _previousSurgeries.Clear();
+ View(ViewType.Parts);
+ };
+
+ _window.SurgeriesButton.OnPressed += _ =>
+ {
+ _surgery = null;
+ _previousSurgeries.Clear();
+
+ if (!_entities.TryGetNetEntity(_part, out var netPart)
+ || State is not SurgeryBuiState s
+ || !s.Choices.TryGetValue(netPart.Value, out var surgeries))
+ return;
+
+ OnPartPressed(netPart.Value, surgeries);
+ };
+
+ _window.StepsButton.OnPressed += _ =>
+ {
+ if (!_entities.TryGetNetEntity(_part, out var netPart)
+ || _previousSurgeries.Count == 0)
+ return;
+
+ var last = _previousSurgeries[^1];
+ _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1);
+
+ if (_system.GetSingleton(last) is not { } previousId
+ || !_entities.TryGetComponent(previousId, out SurgeryComponent? previous))
+ return;
+
+ OnSurgeryPressed((previousId, previous), netPart.Value, last);
+ };
+ }
+
+ _window.Surgeries.DisposeAllChildren();
+ _window.Steps.DisposeAllChildren();
+ _window.Parts.DisposeAllChildren();
+ View(ViewType.Parts);
+
+ var oldSurgery = _surgery;
+ var oldPart = _part;
+ _part = null;
+ _surgery = null;
+
+ var options = new List<(NetEntity netEntity, EntityUid entity, string Name, BodyPartType? PartType)>();
+ foreach (var choice in state.Choices.Keys)
+ if (_entities.TryGetEntity(choice, out var ent))
+ {
+ if (_entities.TryGetComponent(ent, out BodyPartComponent? part))
+ options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, part.PartType));
+ else if (_entities.TryGetComponent(ent, out BodyComponent? body))
+ options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, null));
+ }
+
+ options.Sort((a, b) =>
+ {
+ int GetScore(BodyPartType? partType)
+ {
+ return partType switch
+ {
+ BodyPartType.Head => 1,
+ BodyPartType.Torso => 2,
+ BodyPartType.Arm => 3,
+ BodyPartType.Hand => 4,
+ BodyPartType.Leg => 5,
+ BodyPartType.Foot => 6,
+ // BodyPartType.Tail => 7, No tails yet!
+ BodyPartType.Other => 8,
+ _ => 9
+ };
+ }
+
+ return GetScore(a.PartType) - GetScore(b.PartType);
+ });
+
+ foreach (var (netEntity, entity, partName, _) in options)
+ {
+ //var netPart = _entities.GetNetEntity(part.Owner);
+ var surgeries = state.Choices[netEntity];
+ var partButton = new XenoChoiceControl();
+
+ partButton.Set(partName, null);
+ partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
+
+ _window.Parts.AddChild(partButton);
+
+ foreach (var surgeryId in surgeries)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ continue;
+
+ if (oldPart == entity && oldSurgery?.Proto == surgeryId)
+ OnSurgeryPressed((surgery, surgeryComp), netEntity, surgeryId);
+ }
+
+ if (oldPart == entity && oldSurgery == null)
+ OnPartPressed(netEntity, surgeries);
+ }
+
+
+ if (!_window.IsOpen)
+ _window.OpenCentered();
+ }
+
+ private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null
+ || _system.GetSingleton(stepId) is not { } step)
+ return;
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(step).EntityName);
+ var stepButton = new SurgeryStepButton { Step = step };
+ stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg(netPart, surgeryId, stepId, _isBody));
+
+ _window.Steps.AddChild(stepButton);
+ }
+
+ private void OnSurgeryPressed(Entity surgery, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+ _isBody = _entities.HasComponent(_part);
+ _surgery = (surgery, surgeryId);
+
+ _window.Steps.DisposeAllChildren();
+
+ // This apparently does not consider if theres multiple surgery requirements in one surgery. Maybe thats fine.
+ if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement)
+ {
+ var label = new XenoChoiceControl();
+ label.Button.OnPressed += _ =>
+ {
+ _previousSurgeries.Add(surgeryId);
+
+ if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp))
+ OnSurgeryPressed((requirement, requirementComp), netPart, requirementId);
+ };
+
+ var msg = new FormattedMessage();
+ var surgeryName = _entities.GetComponent(requirement).EntityName;
+ msg.AddMarkup($"[bold]{Loc.GetString("surgery-ui-window-require")}: {surgeryName}[/bold]");
+ label.Set(msg, null);
+
+ _window.Steps.AddChild(label);
+ _window.Steps.AddChild(new HSeparator { Margin = new Thickness(0, 0, 0, 1) });
+ }
+ foreach (var stepId in surgery.Comp.Steps)
+ AddStep(stepId, netPart, surgeryId);
+
+ View(ViewType.Steps);
+ RefreshUI();
+ }
+
+ private void OnPartPressed(NetEntity netPart, List surgeryIds)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+ _isBody = _entities.HasComponent(_part);
+ _window.Surgeries.DisposeAllChildren();
+
+ var surgeries = new List<(Entity Ent, EntProtoId Id, string Name)>();
+ foreach (var surgeryId in surgeryIds)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ {
+ continue;
+ }
+
+ var name = _entities.GetComponent(surgery).EntityName;
+ surgeries.Add(((surgery, surgeryComp), surgeryId, name));
+ }
+
+ surgeries.Sort((a, b) =>
+ {
+ var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority);
+ if (priority != 0)
+ return priority;
+
+ return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
+ });
+
+ foreach (var surgery in surgeries)
+ {
+ var surgeryButton = new XenoChoiceControl();
+ surgeryButton.Set(surgery.Name, null);
+
+ surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(surgery.Ent, netPart, surgery.Id);
+ _window.Surgeries.AddChild(surgeryButton);
+ }
+
+ RefreshUI();
+ View(ViewType.Surgeries);
+ }
+
+ private void RefreshUI()
+ {
+ if (_window == null
+ || !_window.IsOpen
+ || _part == null
+ || !_entities.HasComponent(_surgery?.Ent)
+ || !_entities.TryGetComponent(_player.LocalEntity ?? EntityUid.Invalid, out SurgeryTargetComponent? surgeryComp)
+ || !surgeryComp.CanOperate)
+ return;
+
+ var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent);
+ var i = 0;
+ foreach (var child in _window.Steps.Children)
+ {
+ if (child is not SurgeryStepButton stepButton)
+ continue;
+
+ var status = StepStatus.Incomplete;
+ if (next == null)
+ status = StepStatus.Complete;
+ else if (next.Value.Surgery.Owner != _surgery.Value.Ent)
+ status = StepStatus.Incomplete;
+ else if (next.Value.Step == i)
+ status = StepStatus.Next;
+ else if (i < next.Value.Step)
+ status = StepStatus.Complete;
+
+ stepButton.Button.Disabled = status != StepStatus.Next;
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(stepButton.Step).EntityName);
+
+ if (status == StepStatus.Complete)
+ stepButton.Button.Modulate = Color.Green;
+ else
+ {
+ stepButton.Button.Modulate = Color.White;
+ if (_player.LocalEntity is { } player
+ && status == StepStatus.Next
+ && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _))
+ stepButton.ToolTip = popup;
+ }
+
+ var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default;
+ stepButton.Set(stepName, texture);
+ i++;
+ }
+ }
+
+ private void View(ViewType type)
+ {
+ if (_window == null)
+ return;
+
+ _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10);
+
+ _window.Parts.Visible = type == ViewType.Parts;
+ _window.PartsButton.Disabled = type == ViewType.Parts;
+
+ _window.Surgeries.Visible = type == ViewType.Surgeries;
+ _window.SurgeriesButton.Disabled = type != ViewType.Steps;
+
+ _window.Steps.Visible = type == ViewType.Steps;
+ _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0;
+
+ if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) &&
+ _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta))
+ _window.Title = $"Surgery - {partMeta.EntityName}, {surgeryMeta.EntityName}";
+ else if (partMeta != null)
+ _window.Title = $"Surgery - {partMeta.EntityName}";
+ else
+ _window.Title = "Surgery";
+ }
+
+ private enum ViewType
+ {
+ Parts,
+ Surgeries,
+ Steps
+ }
+
+ private enum StepStatus
+ {
+ Next,
+ Complete,
+ Incomplete
+ }
+}
diff --git a/Content.Client/Medical/Surgery/SurgeryStepButton.xaml b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml
new file mode 100644
index 00000000000..7fbf9543e94
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs
new file mode 100644
index 00000000000..31bf1e0752b
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryStepButton.xaml.cs
@@ -0,0 +1,16 @@
+using Content.Client.Xenonids.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Medical.Surgery;
+
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryStepButton : XenoChoiceControl
+{
+ public EntityUid Step { get; set; }
+
+ public SurgeryStepButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Medical/Surgery/SurgerySystem.cs b/Content.Client/Medical/Surgery/SurgerySystem.cs
new file mode 100644
index 00000000000..cbf1aeee483
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgerySystem.cs
@@ -0,0 +1,11 @@
+using Content.Shared.Medical.Surgery;
+
+namespace Content.Client.Medical.Surgery;
+
+public sealed class SurgerySystem : SharedSurgerySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+}
diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml b/Content.Client/Medical/Surgery/SurgeryWindow.xaml
new file mode 100644
index 00000000000..bba801a8a58
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs b/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs
new file mode 100644
index 00000000000..1b579e7408d
--- /dev/null
+++ b/Content.Client/Medical/Surgery/SurgeryWindow.xaml.cs
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Medical.Surgery;
+
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryWindow : DefaultWindow
+{
+ public SurgeryWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index b28af75c762..c89659c3ab5 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -252,6 +252,14 @@ void AddCheckBox(string checkBoxName, bool currentState, Action().Class("TargetDollButtonHead")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/head_hover.png")),
+
+ Element().Class("TargetDollButtonChest")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/torso_hover.png")),
+
+ Element().Class("TargetDollButtonGroin")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/groin_hover.png")),
+
+ Element().Class("TargetDollButtonLeftArm")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftarm_hover.png")),
+
+ Element().Class("TargetDollButtonLeftHand")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/lefthand_hover.png")),
+
+ Element().Class("TargetDollButtonRightArm")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightarm_hover.png")),
+
+ Element().Class("TargetDollButtonRightHand")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/righthand_hover.png")),
+
+ Element().Class("TargetDollButtonLeftLeg")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftleg_hover.png")),
+
+ Element().Class("TargetDollButtonLeftFoot")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/leftfoot_hover.png")),
+
+ Element().Class("TargetDollButtonRightLeg")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightleg_hover.png")),
+
+ Element().Class("TargetDollButtonRightFoot")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/rightfoot_hover.png")),
+
+ Element().Class("TargetDollButtonEyes")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/eyes_hover.png")),
+
+ Element().Class("TargetDollButtonMouth")
+ .Pseudo(TextureButton.StylePseudoClassHover)
+ .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Targeting/Doll/mouth_hover.png")),
+ // Shitmed Edit End
+
}).ToList());
}
}
diff --git a/Content.Client/Targeting/TargetingSystem.cs b/Content.Client/Targeting/TargetingSystem.cs
new file mode 100644
index 00000000000..2c92d53ae1f
--- /dev/null
+++ b/Content.Client/Targeting/TargetingSystem.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Input;
+using Content.Shared.Targeting;
+using Content.Shared.Targeting.Events;
+using Robust.Client.Player;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Player;
+
+namespace Content.Client.Targeting;
+public sealed class TargetingSystem : SharedTargetingSystem
+{
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ public event Action? TargetingStartup;
+ public event Action? TargetingShutdown;
+ public event Action? TargetChange;
+ public event Action? PartStatusStartup;
+ public event Action? PartStatusUpdate;
+ public event Action? PartStatusShutdown;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(HandlePlayerAttached);
+ SubscribeLocalEvent(HandlePlayerDetached);
+ SubscribeLocalEvent(OnTargetingStartup);
+ SubscribeLocalEvent(OnTargetingShutdown);
+ SubscribeNetworkEvent(OnTargetIntegrityChange);
+
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.TargetHead,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.Head)))
+ .Bind(ContentKeyFunctions.TargetTorso,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.Torso)))
+ .Bind(ContentKeyFunctions.TargetLeftArm,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftArm)))
+/* .Bind(ContentKeyFunctions.TargetLeftHand,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftHand))) SOON :TM: */
+ .Bind(ContentKeyFunctions.TargetRightArm,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightArm)))
+/* .Bind(ContentKeyFunctions.TargetRightHand,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightHand)))*/
+ .Bind(ContentKeyFunctions.TargetLeftLeg,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftLeg)))
+/* .Bind(ContentKeyFunctions.TargetLeftFoot,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.LeftFoot)))*/
+ .Bind(ContentKeyFunctions.TargetRightLeg,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightLeg)))
+/* .Bind(ContentKeyFunctions.TargetRightFoot,
+ InputCmdHandler.FromDelegate((session) => HandleTargetChange(session, TargetBodyPart.RightFoot)))*/
+ .Register();
+ }
+
+ private void HandlePlayerAttached(EntityUid uid, TargetingComponent component, LocalPlayerAttachedEvent args)
+ {
+ TargetingStartup?.Invoke(component);
+ PartStatusStartup?.Invoke(component);
+ }
+
+ private void HandlePlayerDetached(EntityUid uid, TargetingComponent component, LocalPlayerDetachedEvent args)
+ {
+ TargetingShutdown?.Invoke();
+ PartStatusShutdown?.Invoke();
+ }
+
+ private void OnTargetingStartup(EntityUid uid, TargetingComponent component, ComponentStartup args)
+ {
+ if (_playerManager.LocalEntity != uid)
+ return;
+
+ TargetingStartup?.Invoke(component);
+ PartStatusStartup?.Invoke(component);
+ }
+
+ private void OnTargetingShutdown(EntityUid uid, TargetingComponent component, ComponentShutdown args)
+ {
+ if (_playerManager.LocalEntity != uid)
+ return;
+
+ TargetingShutdown?.Invoke();
+ PartStatusShutdown?.Invoke();
+ }
+
+ private void OnTargetIntegrityChange(TargetIntegrityChangeEvent args)
+ {
+ if (!TryGetEntity(args.Uid, out var uid)
+ || !_playerManager.LocalEntity.Equals(uid)
+ || !TryComp(uid, out TargetingComponent? component)
+ || !args.RefreshUi)
+ return;
+
+ PartStatusUpdate?.Invoke(component);
+ }
+
+ private void HandleTargetChange(ICommonSession? session, TargetBodyPart target)
+ {
+ if (session == null
+ || session.AttachedEntity is not { } uid
+ || !TryComp(uid, out var targeting))
+ return;
+
+ TargetChange?.Invoke(target);
+ }
+}
diff --git a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
index 4ba820b3392..0596009ed12 100644
--- a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
+++ b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml
@@ -9,6 +9,7 @@
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
+ xmlns:targeting="clr-namespace:Content.Client.UserInterface.Systems.Targeting.Widgets"
Name="DefaultHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
@@ -28,6 +29,7 @@
+
diff --git a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
index c45ec9d4a02..d6bc0f97cc6 100644
--- a/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
+++ b/Content.Client/UserInterface/Screens/OverlayChatGameScreen.xaml.cs
@@ -22,6 +22,7 @@ public OverlayChatGameScreen()
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);
+ SetAnchorAndMarginPreset(Targeting, LayoutPreset.BottomRight, margin: 5);
Chat.OnResized += ChatOnResized;
Chat.OnChatResizeFinish += ChatOnResizeFinish;
diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
index 7f1d1bcd5b1..c60b6c44dda 100644
--- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
+++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml
@@ -10,6 +10,7 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
+ xmlns:targeting="clr-namespace:Content.Client.UserInterface.Systems.Targeting.Widgets"
Name="SeparatedChatHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
@@ -20,6 +21,7 @@
+
diff --git a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
index 45a29e03f1d..5c612587ed3 100644
--- a/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
+++ b/Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml.cs
@@ -23,6 +23,7 @@ public SeparatedChatGameScreen()
SetAnchorAndMarginPreset(Ghost, LayoutPreset.BottomWide, margin: 80);
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.CenterRight, margin: 10);
+ SetAnchorAndMarginPreset(Targeting, LayoutPreset.BottomRight, margin: 5);
ScreenContainer.OnSplitResizeFinished += () =>
OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
index 9a273d2ed14..e52e2175b27 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
+++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml
@@ -1,7 +1,11 @@
-
-
-
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
index fb747799170..347a5023b9f 100644
--- a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
@@ -134,10 +134,9 @@ private void UpdateInventoryHotbar(InventorySlotsComponent? clientInv)
_inventoryHotbar?.ClearButtons();
return;
}
-
foreach (var (_, data) in clientInv.SlotData)
{
- if (!data.ShowInWindow || !_slotGroups.TryGetValue(data.SlotGroup, out var container))
+ if (!data.ShowInWindow || data.SlotDef.Disabled || !_slotGroups.TryGetValue(data.SlotGroup, out var container))
continue;
if (!container.TryGetButton(data.SlotName, out var button))
@@ -210,7 +209,6 @@ private void UpdateStrippingWindow(InventorySlotsComponent? clientInv)
{
if (!data.ShowInWindow)
continue;
-
if (!_strippingWindow!.InventoryButtons.TryGetButton(data.SlotName, out var button))
{
button = CreateSlotButton(data);
diff --git a/Content.Client/UserInterface/Systems/PartStatus/PartStatusUIController.cs b/Content.Client/UserInterface/Systems/PartStatus/PartStatusUIController.cs
new file mode 100644
index 00000000000..a5a0dd830f9
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/PartStatus/PartStatusUIController.cs
@@ -0,0 +1,82 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.PartStatus.Widgets;
+using Content.Shared.Targeting;
+using Content.Client.Targeting;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.Player;
+using Robust.Shared.Utility;
+using Robust.Client.Graphics;
+
+
+namespace Content.Client.UserInterface.Systems.PartStatus;
+
+public sealed class PartStatusUIController : UIController, IOnStateEntered, IOnSystemChanged
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IEntityNetworkManager _net = default!;
+ private SpriteSystem _spriteSystem = default!;
+ private TargetingComponent? _targetingComponent;
+ private PartStatusControl? PartStatusControl => UIManager.GetActiveUIWidgetOrNull();
+
+ public void OnSystemLoaded(TargetingSystem system)
+ {
+ system.PartStatusStartup += AddPartStatusControl;
+ system.PartStatusShutdown += RemovePartStatusControl;
+ system.PartStatusUpdate += UpdatePartStatusControl;
+ }
+
+ public void OnSystemUnloaded(TargetingSystem system)
+ {
+ system.PartStatusStartup -= AddPartStatusControl;
+ system.PartStatusShutdown -= RemovePartStatusControl;
+ system.PartStatusUpdate -= UpdatePartStatusControl;
+ }
+
+ public void OnStateEntered(GameplayState state)
+ {
+ if (PartStatusControl != null)
+ {
+ PartStatusControl.SetVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+ }
+
+ public void AddPartStatusControl(TargetingComponent component)
+ {
+ _targetingComponent = component;
+
+ if (PartStatusControl != null)
+ {
+ PartStatusControl.SetVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+
+ }
+
+ public void RemovePartStatusControl()
+ {
+ if (PartStatusControl != null)
+ PartStatusControl.SetVisible(false);
+
+ _targetingComponent = null;
+ }
+
+ public void UpdatePartStatusControl(TargetingComponent component)
+ {
+ if (PartStatusControl != null && _targetingComponent != null)
+ PartStatusControl.SetTextures(_targetingComponent.BodyStatus);
+ }
+
+ public Texture GetTexture(SpriteSpecifier specifier)
+ {
+ if (_spriteSystem == null)
+ _spriteSystem = _entManager.System();
+
+ return _spriteSystem.Frame0(specifier);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml
new file mode 100644
index 00000000000..f37d7e1cbfe
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs
new file mode 100644
index 00000000000..9c13e50ed14
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/PartStatus/Widgets/PartStatusControl.xaml.cs
@@ -0,0 +1,50 @@
+using Content.Client.UserInterface.Systems.PartStatus;
+using Content.Shared.Targeting;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+using System.Linq;
+
+namespace Content.Client.UserInterface.Systems.PartStatus.Widgets;
+
+[GenerateTypedNameReferences]
+public sealed partial class PartStatusControl : UIWidget
+{
+ private readonly Dictionary _partStatusControls;
+ private readonly PartStatusUIController _controller;
+ public PartStatusControl()
+ {
+ RobustXamlLoader.Load(this);
+
+ _controller = UserInterfaceManager.GetUIController();
+ _partStatusControls = new Dictionary
+ {
+ { TargetBodyPart.Head, DollHead },
+ { TargetBodyPart.Torso, DollTorso },
+ { TargetBodyPart.Groin, DollGroin },
+ { TargetBodyPart.LeftArm, DollLeftArm },
+ { TargetBodyPart.LeftHand, DollLeftHand },
+ { TargetBodyPart.RightArm, DollRightArm },
+ { TargetBodyPart.RightHand, DollRightHand },
+ { TargetBodyPart.LeftLeg, DollLeftLeg },
+ { TargetBodyPart.LeftFoot, DollLeftFoot },
+ { TargetBodyPart.RightLeg, DollRightLeg },
+ { TargetBodyPart.RightFoot, DollRightFoot }
+ };
+ }
+
+ public void SetTextures(Dictionary state)
+ {
+ foreach (var (bodyPart, integrity) in state)
+ {
+ string enumName = Enum.GetName(typeof(TargetBodyPart), bodyPart) ?? "Unknown";
+ int enumValue = (int) integrity;
+ var texture = new SpriteSpecifier.Rsi(new ResPath($"/Textures/Interface/Targeting/Status/{enumName.ToLowerInvariant()}.rsi"), $"{enumName.ToLowerInvariant()}_{enumValue}");
+ _partStatusControls[bodyPart].Texture = _controller.GetTexture(texture);
+ }
+ }
+
+ public void SetVisible(bool visible) => this.Visible = visible;
+
+}
diff --git a/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs b/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs
new file mode 100644
index 00000000000..d430adfd9ca
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/TargetingUIController.cs
@@ -0,0 +1,82 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.Targeting.Widgets;
+using Content.Shared.Targeting;
+using Content.Client.Targeting;
+using Content.Shared.Targeting.Events;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Client.Player;
+
+namespace Content.Client.UserInterface.Systems.Targeting;
+
+public sealed class TargetingUIController : UIController, IOnStateEntered, IOnSystemChanged
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IEntityNetworkManager _net = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ private TargetingComponent? _targetingComponent;
+ private TargetingControl? TargetingControl => UIManager.GetActiveUIWidgetOrNull();
+
+ public void OnSystemLoaded(TargetingSystem system)
+ {
+ system.TargetingStartup += AddTargetingControl;
+ system.TargetingShutdown += RemoveTargetingControl;
+ system.TargetChange += CycleTarget;
+ }
+
+ public void OnSystemUnloaded(TargetingSystem system)
+ {
+ system.TargetingStartup -= AddTargetingControl;
+ system.TargetingShutdown -= RemoveTargetingControl;
+ system.TargetChange -= CycleTarget;
+ }
+
+ public void OnStateEntered(GameplayState state)
+ {
+ if (TargetingControl == null)
+ return;
+
+ TargetingControl.SetTargetDollVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ TargetingControl.SetBodyPartsVisible(_targetingComponent.Target);
+ }
+
+ public void AddTargetingControl(TargetingComponent component)
+ {
+ _targetingComponent = component;
+
+ if (TargetingControl != null)
+ {
+ TargetingControl.SetTargetDollVisible(_targetingComponent != null);
+
+ if (_targetingComponent != null)
+ TargetingControl.SetBodyPartsVisible(_targetingComponent.Target);
+ }
+
+ }
+
+ public void RemoveTargetingControl()
+ {
+ if (TargetingControl != null)
+ TargetingControl.SetTargetDollVisible(false);
+
+ _targetingComponent = null;
+ }
+
+ public void CycleTarget(TargetBodyPart bodyPart)
+ {
+ if (_playerManager.LocalEntity is not { } user
+ || _entManager.GetComponent(user) is not { } targetingComponent
+ || TargetingControl == null)
+ return;
+
+ var player = _entManager.GetNetEntity(user);
+ if (bodyPart != targetingComponent.Target)
+ {
+ var msg = new TargetChangeEvent(player, bodyPart);
+ _net.SendSystemNetworkMessage(msg);
+ TargetingControl?.SetBodyPartsVisible(bodyPart);
+ }
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml
new file mode 100644
index 00000000000..1489628d932
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs
new file mode 100644
index 00000000000..07af0e8092a
--- /dev/null
+++ b/Content.Client/UserInterface/Systems/Targeting/Widgets/TargetingControl.xaml.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using Content.Shared.Targeting;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.UserInterface.Systems.Targeting.Widgets;
+
+[GenerateTypedNameReferences]
+public sealed partial class TargetingControl : UIWidget
+{
+ private readonly TargetingUIController _controller;
+ private readonly Dictionary _bodyPartControls;
+
+ public TargetingControl()
+ {
+ RobustXamlLoader.Load(this);
+ _controller = UserInterfaceManager.GetUIController();
+
+ _bodyPartControls = new Dictionary
+ {
+ // TODO: ADD EYE AND MOUTH TARGETING
+ { TargetBodyPart.Head, HeadButton },
+ { TargetBodyPart.Torso, ChestButton },
+ { TargetBodyPart.Groin, GroinButton },
+ { TargetBodyPart.LeftArm, LeftArmButton },
+ { TargetBodyPart.LeftHand, LeftHandButton },
+ { TargetBodyPart.RightArm, RightArmButton },
+ { TargetBodyPart.RightHand, RightHandButton },
+ { TargetBodyPart.LeftLeg, LeftLegButton },
+ { TargetBodyPart.LeftFoot, LeftFootButton },
+ { TargetBodyPart.RightLeg, RightLegButton },
+ { TargetBodyPart.RightFoot, RightFootButton },
+ };
+
+ foreach (var bodyPartButton in _bodyPartControls)
+ {
+ bodyPartButton.Value.MouseFilter = MouseFilterMode.Stop;
+ bodyPartButton.Value.OnPressed += _ => SetActiveBodyPart(bodyPartButton.Key);
+
+ TargetDoll.Texture = Theme.ResolveTexture("target_doll");
+ }
+ }
+
+ private void SetActiveBodyPart(TargetBodyPart bodyPart) => _controller.CycleTarget(bodyPart);
+
+ public void SetBodyPartsVisible(TargetBodyPart bodyPart)
+ {
+ foreach (var bodyPartButton in _bodyPartControls)
+ bodyPartButton.Value.Children.First().Visible = bodyPartButton.Key == bodyPart;
+ }
+
+ protected override void OnThemeUpdated() => TargetDoll.Texture = Theme.ResolveTexture("target_doll");
+
+ public void SetTargetDollVisible(bool visible) => Visible = visible;
+
+}
diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml
new file mode 100644
index 00000000000..1257fbc54b4
--- /dev/null
+++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs
new file mode 100644
index 00000000000..ae451fffe01
--- /dev/null
+++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs
@@ -0,0 +1,26 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Xenonids.UI;
+
+[GenerateTypedNameReferences]
+[Virtual]
+public partial class XenoChoiceControl : Control
+{
+ public XenoChoiceControl() => RobustXamlLoader.Load(this);
+
+ public void Set(string name, Texture? texture)
+ {
+ NameLabel.SetMessage(name);
+ Texture.Texture = texture;
+ }
+
+ public void Set(FormattedMessage msg, Texture? texture)
+ {
+ NameLabel.SetMessage(msg);
+ Texture.Texture = texture;
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Shitmed/Body/SpeciesBUiTest.cs b/Content.IntegrationTests/Tests/Shitmed/Body/SpeciesBUiTest.cs
new file mode 100644
index 00000000000..f82a22d4471
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Shitmed/Body/SpeciesBUiTest.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Backmen.Body;
+
+[TestFixture]
+public sealed class SpeciesBUiTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ name: BaseMobSpeciesTest
+ id: BaseMobSpeciesTest
+ parent: BaseMobSpecies
+";
+
+ private Dictionary GetInterfaces(UserInterfaceComponent comp) =>
+ (Dictionary)
+ typeof(UserInterfaceComponent).GetField("Interfaces", BindingFlags.NonPublic | BindingFlags.Instance)!
+ .GetValue(comp);
+
+ [Test]
+ public async Task AllSpeciesHaveBaseBUiTest()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Dirty = true,
+ Connected = false
+ });
+
+ var server = pair.Server;
+ var proto = server.ResolveDependency();
+ var factoryComp = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ var bUiSys = server.System();
+
+ Assert.That(proto.TryIndex("BaseMobSpeciesTest", out var baseEnt), Is.True);
+ Assert.That(baseEnt, Is.Not.Null);
+ Assert.That(baseEnt.TryGetComponent(out var bUiBase, factoryComp), Is.True);
+ Assert.That(bUiBase, Is.Not.Null);
+ var baseKeys = GetInterfaces(bUiBase).Keys.ToArray();
+
+ foreach (var species in proto.EnumeratePrototypes())
+ {
+ var ent = proto.Index(species.Prototype);
+ Assert.That(ent.TryGetComponent(out var bUi, factoryComp), Is.True);
+ Assert.That(bUi, Is.Not.Null);
+ var states = GetInterfaces(bUiBase);
+ foreach (var key in baseKeys)
+ {
+ Assert.That(states.ContainsKey(key), Is.True, $"Species {species.ID} has not UserInterface of type enum.{key.GetType().Name}");
+ }
+ }
+ });
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
index 9bea58330cd..35f5ec8a3bc 100644
--- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
@@ -234,7 +234,7 @@ public override void Update(float frameTime)
if (pressure <= Atmospherics.HazardLowPressure)
{
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
- _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false);
+ _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * Atmospherics.LowPressureDamage, true, false, canSever: false);
if (!barotrauma.TakingDamage)
{
barotrauma.TakingDamage = true;
@@ -247,7 +247,7 @@ public override void Update(float frameTime)
{
var damageScale = MathF.Min(((pressure / Atmospherics.HazardHighPressure) - 1) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
- _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false);
+ _damageableSystem.TryChangeDamage(uid, barotrauma.Damage * damageScale, true, false, canSever: false);
RaiseLocalEvent(uid, new MoodEffectEvent("MobHighPressure"));
if (!barotrauma.TakingDamage)
diff --git a/Content.Server/Bed/Sleep/SleepingSystem.cs b/Content.Server/Bed/Sleep/SleepingSystem.cs
index 5e4f0eddb52..4edb2ed28de 100644
--- a/Content.Server/Bed/Sleep/SleepingSystem.cs
+++ b/Content.Server/Bed/Sleep/SleepingSystem.cs
@@ -94,8 +94,13 @@ private void OnDamageChanged(EntityUid uid, SleepingComponent component, DamageC
if (!args.DamageIncreased || args.DamageDelta == null)
return;
- if (args.DamageDelta.GetTotal() >= component.WakeThreshold)
+ /* Surgery needs this, sorry! If the nocturine gamers get too feisty
+ I'll probably just increase the threshold */
+
+ if (args.DamageDelta.GetTotal() >= component.WakeThreshold
+ && !HasComp(uid))
TryWaking(uid, component);
+
}
private void OnSleepAction(EntityUid uid, MobStateComponent component, SleepActionEvent args)
diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs
index 37f78ed81a0..eb0c1df6526 100644
--- a/Content.Server/Body/Systems/BodySystem.cs
+++ b/Content.Server/Body/Systems/BodySystem.cs
@@ -1,9 +1,12 @@
+using System.Linq;
using Content.Server.Body.Components;
using Content.Server.GameTicking;
using Content.Server.Humanoid;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
+using Content.Shared.Damage;
+using Content.Shared.Gibbing.Events;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
@@ -22,8 +25,8 @@ public sealed class BodySystem : SharedBodySystem
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public override void Initialize()
@@ -96,8 +99,10 @@ protected override void RemovePart(
return;
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
+
_humanoidSystem.SetLayersVisibility(
bodyEnt, layers, visible: false, permanent: true, humanoid);
+ _appearance.SetData(bodyEnt, layer, true);
}
public override HashSet GibBody(
@@ -108,8 +113,9 @@ public override HashSet GibBody(
Vector2? splatDirection = null,
float splatModifier = 1,
Angle splatCone = default,
- SoundSpecifier? gibSoundOverride = null
- )
+ SoundSpecifier? gibSoundOverride = null,
+ GibType gib = GibType.Gib,
+ GibContentsOption contents = GibContentsOption.Drop)
{
if (!Resolve(bodyId, ref body, logMissing: false)
|| TerminatingOrDeleted(bodyId)
@@ -123,7 +129,8 @@ public override HashSet GibBody(
return new HashSet();
var gibs = base.GibBody(bodyId, gibOrgans, body, launchGibs: launchGibs,
- splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone);
+ splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone,
+ gib: gib, contents: contents);
var ev = new BeingGibbedEvent(gibs);
RaiseLocalEvent(bodyId, ref ev);
@@ -132,4 +139,46 @@ public override HashSet GibBody(
return gibs;
}
+
+ public override HashSet GibPart(
+ EntityUid partId,
+ BodyPartComponent? part = null,
+ bool launchGibs = true,
+ Vector2? splatDirection = null,
+ float splatModifier = 1,
+ Angle splatCone = default,
+ SoundSpecifier? gibSoundOverride = null)
+ {
+ if (!Resolve(partId, ref part, logMissing: false)
+ || TerminatingOrDeleted(partId)
+ || EntityManager.IsQueuedForDeletion(partId))
+ return new HashSet();
+
+ if (Transform(partId).MapUid is null)
+ return new HashSet();
+
+ var gibs = base.GibPart(partId, part, launchGibs: launchGibs,
+ splatDirection: splatDirection, splatModifier: splatModifier, splatCone: splatCone);
+
+ var ev = new BeingGibbedEvent(gibs);
+ RaiseLocalEvent(partId, ref ev);
+
+ QueueDel(partId);
+
+ return gibs;
+ }
+
+ protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component)
+ {
+ return;
+ }
+
+ protected override void RemoveBodyMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance)
+ {
+ foreach (var (visualLayer, markingList) in partAppearance.Markings)
+ foreach (var marking in markingList)
+ _humanoidSystem.RemoveMarking(target, marking.MarkingId, sync: false, humanoid: bodyAppearance);
+
+ Dirty(target, bodyAppearance);
+ }
}
diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs
index 86d2cb61ffe..ae14da0e817 100644
--- a/Content.Server/Body/Systems/BrainSystem.cs
+++ b/Content.Server/Body/Systems/BrainSystem.cs
@@ -2,6 +2,7 @@
using Content.Server.Ghost.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
+using Content.Shared.Body.Organ;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Pointing;
@@ -12,15 +13,34 @@ public sealed class BrainSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
+ // Shitmed-Start
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent((uid, _, args) => HandleMind(args.Body, uid));
- SubscribeLocalEvent((uid, _, args) => HandleMind(uid, args.OldBody));
+ SubscribeLocalEvent(HandleAddition);
+ SubscribeLocalEvent(HandleRemoval);
SubscribeLocalEvent(OnPointAttempt);
}
+ private void HandleRemoval(EntityUid uid, BrainComponent _, ref OrganRemovedFromBodyEvent args)
+ {
+ if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(args.OldBody))
+ return;
+
+ // Prevents revival, should kill the user within a given timespan too.
+ EnsureComp(args.OldBody);
+ HandleMind(uid, args.OldBody);
+ }
+ private void HandleAddition(EntityUid uid, BrainComponent _, ref OrganAddedToBodyEvent args)
+ {
+ if (TerminatingOrDeleted(uid) || TerminatingOrDeleted(args.Body))
+ return;
+
+ RemComp(args.Body);
+ HandleMind(args.Body, uid);
+ }
+ // Shitmed-End
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
{
if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity))
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index f6a17d32b65..8b2b19a2ac0 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -7,6 +7,7 @@
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
+using Content.Shared.Body.Organ;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Mobs.Systems;
@@ -71,7 +72,7 @@ public override void Update(float frameTime)
UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator);
- if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
+ if (!_mobState.IsIncapacitated(uid) || HasComp(uid)) // Shitmed: cannot breathe in crit or when no brain.
{
switch (respirator.Status)
{
diff --git a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
index 24880cfd371..53933072b6d 100644
--- a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
+++ b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs
@@ -3,6 +3,7 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Localizations;
+using Content.Shared.Targeting; // Shitmed
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using System.Linq;
@@ -119,7 +120,12 @@ public override void Effect(ReagentEffectArgs args)
args.SolutionEntity,
Damage * scale,
IgnoreResistances,
- interruptsDoAfters: false);
+ interruptsDoAfters: false,
+ // Shitmed Start
+ targetPart: TargetBodyPart.All,
+ partMultiplier: 0.5f,
+ canSever: false);
+ // Shitmed End
}
}
}
diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs
index c83fed19069..da054e24ac3 100644
--- a/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs
+++ b/Content.Server/Destructible/Thresholds/Behaviors/GibBehavior.cs
@@ -1,4 +1,5 @@
using Content.Shared.Body.Components;
+using Content.Shared.Gibbing.Events;
using JetBrains.Annotations;
namespace Content.Server.Destructible.Thresholds.Behaviors
@@ -7,13 +8,15 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
[DataDefinition]
public sealed partial class GibBehavior : IThresholdBehavior
{
+ [DataField] public GibType GibType = GibType.Gib;
+ [DataField] public GibContentsOption GibContents = GibContentsOption.Drop;
[DataField("recursive")] private bool _recursive = true;
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
if (system.EntityManager.TryGetComponent(owner, out BodyComponent? body))
{
- system.BodySystem.GibBody(owner, _recursive, body);
+ system.BodySystem.GibBody(owner, _recursive, body, gib: GibType, contents: GibContents);
}
}
}
diff --git a/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs
new file mode 100644
index 00000000000..f9e39ba8847
--- /dev/null
+++ b/Content.Server/Destructible/Thresholds/Behaviors/GibPartBehavior.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using JetBrains.Annotations;
+
+namespace Content.Server.Destructible.Thresholds.Behaviors;
+
+[UsedImplicitly]
+[DataDefinition]
+public sealed partial class GibPartBehavior : IThresholdBehavior
+{
+ public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
+ {
+ if (!system.EntityManager.TryGetComponent(owner, out BodyPartComponent? part))
+ return;
+
+ system.BodySystem.GibPart(owner, part);
+ }
+}
+
diff --git a/Content.Server/Execution/ExecutionSystem.cs b/Content.Server/Execution/ExecutionSystem.cs
index c1080b84ad8..453a05e8039 100644
--- a/Content.Server/Execution/ExecutionSystem.cs
+++ b/Content.Server/Execution/ExecutionSystem.cs
@@ -250,7 +250,7 @@ private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEven
if (!TryComp(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f)
return;
- _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true);
+ _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true, origin: attacker);
_audioSystem.PlayEntity(melee.SoundHit, Filter.Pvs(weapon), weapon, true, AudioParams.Default);
if (attacker == victim)
@@ -374,7 +374,7 @@ private void OnDoafterGun(EntityUid uid, GunComponent component, DoAfterEvent ar
}
// Gun successfully fired, deal damage
- _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true);
+ _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true, origin: attacker);
_audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default);
// Popups
diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs
index 289c440ab7f..8f0519b24cf 100644
--- a/Content.Server/Hands/Systems/HandsSystem.cs
+++ b/Content.Server/Hands/Systems/HandsSystem.cs
@@ -2,6 +2,8 @@
using Content.Server.Inventory;
using Content.Server.Stack;
using Content.Server.Stunnable;
+using Content.Shared.Body.Systems;
+using Content.Shared.Body.Events;
using Content.Shared.ActionBlocker;
using Content.Shared.Body.Part;
using Content.Shared.CombatMode;
@@ -35,12 +37,12 @@ public sealed class HandsSystem : SharedHandsSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PullingSystem _pullingSystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
-
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnDisarmed, before: new[] {typeof(StunSystem)});
+ SubscribeLocalEvent(OnDisarmed, before: new[] { typeof(StunSystem) });
SubscribeLocalEvent(HandlePullStarted);
SubscribeLocalEvent(HandlePullStopped);
@@ -51,6 +53,8 @@ public override void Initialize()
SubscribeLocalEvent(GetComponentState);
SubscribeLocalEvent(OnExploded);
+ SubscribeLocalEvent(HandleBodyPartEnabled);
+ SubscribeLocalEvent(HandleBodyPartDisabled);
CommandBinds.Builder
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
@@ -98,32 +102,55 @@ private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent a
args.Handled = true; // no shove/stun.
}
- private void HandleBodyPartAdded(EntityUid uid, HandsComponent component, ref BodyPartAddedEvent args)
+ private void TryAddHand(EntityUid uid, HandsComponent component, Entity part, string slot)
{
- if (args.Part.Comp.PartType != BodyPartType.Hand)
+ if (part.Comp is null
+ || part.Comp.PartType != BodyPartType.Hand)
return;
// If this annoys you, which it should.
// Ping Smugleaf.
- var location = args.Part.Comp.Symmetry switch
+ var location = part.Comp.Symmetry switch
{
BodyPartSymmetry.None => HandLocation.Middle,
BodyPartSymmetry.Left => HandLocation.Left,
BodyPartSymmetry.Right => HandLocation.Right,
- _ => throw new ArgumentOutOfRangeException(nameof(args.Part.Comp.Symmetry))
+ _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry))
};
- AddHand(uid, args.Slot, location);
+ if (part.Comp.Enabled
+ && _bodySystem.TryGetParentBodyPart(part, out var _, out var parentPartComp)
+ && parentPartComp.Enabled)
+ AddHand(uid, slot, location);
+ }
+
+ private void HandleBodyPartAdded(EntityUid uid, HandsComponent component, ref BodyPartAddedEvent args)
+ {
+ TryAddHand(uid, component, args.Part, args.Slot);
}
private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args)
{
- if (args.Part.Comp.PartType != BodyPartType.Hand)
+ if (args.Part.Comp is null
+ || args.Part.Comp.PartType != BodyPartType.Hand)
return;
-
RemoveHand(uid, args.Slot);
}
+ private void HandleBodyPartEnabled(EntityUid uid, HandsComponent component, ref BodyPartEnabledEvent args) =>
+ TryAddHand(uid, component, args.Part, SharedBodySystem.GetPartSlotContainerId(args.Part.Comp.ParentSlot?.Id ?? string.Empty));
+
+ private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args)
+ {
+ if (TerminatingOrDeleted(uid)
+ || args.Part.Comp is null
+ || args.Part.Comp.PartType != BodyPartType.Hand)
+ return;
+
+ RemoveHand(uid, SharedBodySystem.GetPartSlotContainerId(args.Part.Comp.ParentSlot?.Id ?? string.Empty));
+ }
+
+
#region pulling
private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args)
@@ -167,7 +194,7 @@ private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStop
private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
{
- if (playerSession?.AttachedEntity is not {Valid: true} player || !Exists(player))
+ if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
return ThrowHeldItem(player, coordinates);
@@ -192,7 +219,7 @@ hands.ActiveHandEntity is not { } throwEnt ||
{
var splitStack = _stackSystem.Split(throwEnt, 1, EntityManager.GetComponent(player).Coordinates, stack);
- if (splitStack is not {Valid: true})
+ if (splitStack is not { Valid: true })
return false;
throwEnt = splitStack.Value;
@@ -204,7 +231,7 @@ hands.ActiveHandEntity is not { } throwEnt ||
var length = direction.Length();
var distance = Math.Clamp(length, minDistance, hands.ThrowRange);
- direction *= distance/length;
+ direction *= distance / length;
var throwStrength = hands.ThrowForceMultiplier;
diff --git a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
index 6380b71c8a0..f0b56cbd195 100644
--- a/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
+++ b/Content.Server/Medical/Components/HealthAnalyzerComponent.cs
@@ -35,6 +35,12 @@ public sealed partial class HealthAnalyzerComponent : Component
[DataField]
public EntityUid? ScannedEntity;
+ ///
+ /// The body part that is currently being scanned.
+ ///
+ [DataField]
+ public EntityUid? CurrentBodyPart;
+
///
/// The maximum range in tiles at which the analyzer can receive continuous updates
///
diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs
index 023a2e4083f..caae6c5f17e 100644
--- a/Content.Server/Medical/CryoPodSystem.cs
+++ b/Content.Server/Medical/CryoPodSystem.cs
@@ -207,6 +207,7 @@ private void OnActivateUI(Entity entity, ref AfterActivatableU
: 0,
null,
null,
+ null,
null
));
}
diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs
index e7362d481ed..c64bb2a485d 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;
@@ -9,6 +10,8 @@
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
+using Content.Shared.Targeting;
+using Content.Shared.Body.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
@@ -16,9 +19,11 @@
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Targeting;
using Content.Shared.Stacks;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
+using System.Linq;
namespace Content.Server.Medical;
@@ -27,6 +32,8 @@ public sealed class HealingSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
+
+ [Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -79,7 +86,7 @@ entity.Comp.DamageContainerID is not null &&
if (healing.ModifyBloodLevel != 0)
_bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel);
- var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.Args.User);
+ var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.User, canSever: false);
if (healed == null && healing.BloodlossModifier != 0)
return;
@@ -113,8 +120,8 @@ entity.Comp.DamageContainerID is not null &&
_audio.PlayPvs(healing.HealingEndSound, entity.Owner, AudioHelpers.WithVariation(0.125f, _random).WithVolume(-5f));
- // Logic to determine the whether or not to repeat the healing action
- args.Repeat = (HasDamage(entity.Comp, healing) && !dontRepeat);
+ // Logic to determine whether or not to repeat the healing action
+ args.Repeat = HasDamage(entity.Comp, healing) && !dontRepeat || IsPartDamaged(args.User, entity);
if (!args.Repeat && !dontRepeat)
_popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User);
args.Handled = true;
@@ -135,6 +142,20 @@ private bool HasDamage(DamageableComponent component, HealingComponent healing)
return false;
}
+ private bool IsPartDamaged(EntityUid user, EntityUid target)
+ {
+ if (!TryComp(user, out TargetingComponent? targeting))
+ return false;
+
+ var (targetType, targetSymmetry) = _bodySystem.ConvertTargetBodyPart(targeting.Target);
+ foreach (var part in _bodySystem.GetBodyChildrenOfType(target, targetType, symmetry: targetSymmetry))
+ if (TryComp(part.Id, out var damageable)
+ && damageable.TotalDamage > part.Component.MinIntegrity)
+ return true;
+
+ return false;
+ }
+
private void OnHealingUse(Entity entity, ref UseInHandEvent args)
{
if (args.Handled)
@@ -173,6 +194,7 @@ targetDamage.DamageContainerID is not null &&
var anythingToDo =
HasDamage(targetDamage, component) ||
+ IsPartDamaged(user, target) ||
component.ModifyBloodLevel > 0 // Special case if healing item can restore lost blood...
&& TryComp(target, out var bloodstream)
&& _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index 76d09c42f08..11d2758cdd8 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -5,6 +5,11 @@
using Content.Server.Temperature.Components;
using Content.Server.Traits.Assorted;
using Content.Shared.Chemistry.EntitySystems;
+// Shitmed Start
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.Targeting;
+// Shitmed End
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
@@ -17,6 +22,7 @@
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Timing;
+using System.Linq;
namespace Content.Server.Medical;
@@ -26,6 +32,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
[Dependency] private readonly PowerCellSystem _cell = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
@@ -37,6 +44,12 @@ public override void Initialize()
SubscribeLocalEvent(OnInsertedIntoContainer);
SubscribeLocalEvent(OnPowerCellSlotEmpty);
SubscribeLocalEvent(OnDropped);
+ // Start-Shitmed
+ Subs.BuiEvents(HealthAnalyzerUiKey.Key, subs =>
+ {
+ subs.Event(OnHealthAnalyzerPartSelected);
+ });
+ // End-Shitmed
}
public override void Update(float frameTime)
@@ -57,6 +70,17 @@ public override void Update(float frameTime)
continue;
}
+ // Shitmed Change Start
+ if (component.CurrentBodyPart != null
+ && (Deleted(component.CurrentBodyPart)
+ || TryComp(component.CurrentBodyPart, out BodyPartComponent? bodyPartComponent)
+ && bodyPartComponent.Body is null))
+ {
+ BeginAnalyzingEntity((uid, component), patient, null);
+ continue;
+ }
+ // Shitmed Change End
+
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
//Get distance between health analyzer and the scanned entity
@@ -68,7 +92,7 @@ public override void Update(float frameTime)
continue;
}
- UpdateScannedUser(uid, patient, true);
+ UpdateScannedUser(uid, patient, true, component.CurrentBodyPart);
}
}
@@ -142,14 +166,14 @@ private void OpenUserInterface(EntityUid user, EntityUid analyzer)
///
/// The health analyzer that should receive the updates
/// The entity to start analyzing
- private void BeginAnalyzingEntity(Entity healthAnalyzer, EntityUid target)
+ private void BeginAnalyzingEntity(Entity healthAnalyzer, EntityUid target, EntityUid? part = null)
{
//Link the health analyzer to the scanned entity
healthAnalyzer.Comp.ScannedEntity = target;
-
+ healthAnalyzer.Comp.CurrentBodyPart = part;
_cell.SetPowerCellDrawEnabled(healthAnalyzer, true);
- UpdateScannedUser(healthAnalyzer, target, true);
+ UpdateScannedUser(healthAnalyzer, target, true, part);
}
///
@@ -161,26 +185,50 @@ private void StopAnalyzingEntity(Entity healthAnalyzer,
{
//Unlink the analyzer
healthAnalyzer.Comp.ScannedEntity = null;
+ healthAnalyzer.Comp.CurrentBodyPart = null;
_cell.SetPowerCellDrawEnabled(target, false);
UpdateScannedUser(healthAnalyzer, target, false);
}
+ // Start-Shitmed
+ ///
+ /// Handle the selection of a body part on the health analyzer
+ ///
+ /// The health analyzer that's receiving the updates
+ /// The message containing the selected part
+ private void OnHealthAnalyzerPartSelected(Entity healthAnalyzer, ref HealthAnalyzerPartMessage args)
+ {
+ if (!TryGetEntity(args.Owner, out var owner))
+ return;
+
+ if (args.BodyPart == null)
+ {
+ BeginAnalyzingEntity(healthAnalyzer, owner.Value, null);
+ }
+ else
+ {
+ var (targetType, targetSymmetry) = _bodySystem.ConvertTargetBodyPart(args.BodyPart.Value);
+ if (_bodySystem.GetBodyChildrenOfType(owner.Value, targetType, symmetry: targetSymmetry) is { } part)
+ BeginAnalyzingEntity(healthAnalyzer, owner.Value, part.FirstOrDefault().Id);
+ }
+ }
+// End-Shitmed
+
///
/// Send an update for the target to the healthAnalyzer
///
/// The health analyzer
/// The entity being scanned
/// True makes the UI show ACTIVE, False makes the UI show INACTIVE
- public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
+ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode, EntityUid? part = null)
{
if (!_uiSystem.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key))
return;
if (!HasComp(target))
return;
-
var bodyTemperature = float.NaN;
if (TryComp(target, out var temp))
@@ -201,13 +249,22 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s
/*if (HasComp(target)) Somehow we dont have unrevivable???
unrevivable = true;
*/
+
+ // Start-Shitmed
+ Dictionary? body = null;
+ if (HasComp(target))
+ body = _bodySystem.GetBodyPartStatus(target);
+ // End-Shitmed
+
_uiSystem.ServerSendUiMessage(healthAnalyzer, HealthAnalyzerUiKey.Key, new HealthAnalyzerScannedUserMessage(
GetNetEntity(target),
bodyTemperature,
bloodAmount,
scanMode,
bleeding,
- unrevivable
+ unrevivable,
+ body, // Shitmed
+ part != null ? GetNetEntity(part) : null // Shitmed
));
}
}
diff --git a/Content.Server/Medical/Surgery/SurgerySystem.cs b/Content.Server/Medical/Surgery/SurgerySystem.cs
new file mode 100644
index 00000000000..615166390a3
--- /dev/null
+++ b/Content.Server/Medical/Surgery/SurgerySystem.cs
@@ -0,0 +1,189 @@
+using Content.Server.Atmos.Rotting;
+using Content.Server.Body.Systems;
+using Content.Server.Chat.Systems;
+using Content.Shared.Body.Organ;
+using Content.Shared.Body.Part;
+using Content.Server.Popups;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.CCVar;
+using Content.Shared.Damage;
+using Content.Shared.Eye.Blinding.Components;
+using Content.Shared.Eye.Blinding.Systems;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Medical.Surgery;
+using Content.Shared.Medical.Surgery.Conditions;
+using Content.Shared.Medical.Surgery.Effects.Step;
+using Content.Shared.Medical.Surgery.Steps;
+using Content.Shared.Medical.Surgery.Steps.Parts;
+using Content.Shared.Medical.Surgery.Tools;
+using Content.Shared.Mood;
+using Content.Shared.Prototypes;
+using Robust.Server.GameObjects;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using System.Linq;
+
+namespace Content.Server.Medical.Surgery;
+
+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 _damageable = 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!;
+
+ private readonly List _surgeries = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnToolAfterInteract);
+ SubscribeLocalEvent(OnSurgeryStepDamage);
+ SubscribeLocalEvent(OnSurgeryDamageChange);
+ SubscribeLocalEvent(OnSurgerySpecialDamageChange);
+ SubscribeLocalEvent(OnStepScreamComplete);
+ SubscribeLocalEvent(OnStepSpawnComplete);
+ SubscribeLocalEvent(OnPrototypesReloaded);
+ LoadPrototypes();
+ }
+
+ protected override void RefreshUI(EntityUid body)
+ {
+ var surgeries = new Dictionary>();
+ foreach (var surgery in _surgeries)
+ {
+ if (GetSingleton(surgery) is not { } surgeryEnt)
+ continue;
+
+ foreach (var part in _body.GetBodyChildren(body))
+ {
+ var ev = new SurgeryValidEvent(body, part.Id);
+ RaiseLocalEvent(surgeryEnt, ref ev);
+
+ if (ev.Cancelled)
+ continue;
+
+ surgeries.GetOrNew(GetNetEntity(part.Id)).Add(surgery);
+ }
+
+ }
+ _ui.SetUiState(body, SurgeryUIKey.Key, new SurgeryBuiState(surgeries));
+ /*
+ Reason we do this is because when applying a BUI State, it rolls back the state on the entity temporarily,
+ which just so happens to occur right as we're checking for step completion, so we end up with the UI
+ not updating at all until you change tools or reopen the window. I love shitcode.
+ */
+ _ui.ServerSendUiMessage(body, SurgeryUIKey.Key, new SurgeryBuiRefreshMessage());
+ }
+ private void SetDamage(EntityUid body,
+ DamageSpecifier damage,
+ float partMultiplier,
+ EntityUid user,
+ EntityUid part)
+ {
+ if (!TryComp(part, out var partComp))
+ return;
+
+ _damageable.TryChangeDamage(body,
+ damage,
+ true,
+ origin: user,
+ canSever: false,
+ partMultiplier: partMultiplier,
+ targetPart: _body.GetTargetBodyPart(partComp));
+ }
+
+ private void OnToolAfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ var user = args.User;
+ if (args.Handled
+ || !args.CanReach
+ || args.Target == null
+ || !HasComp(args.Target)
+ || !TryComp(args.User, out var surgery)
+ || !surgery.CanOperate
+ || !IsLyingDown(args.Target.Value, args.User))
+ {
+ return;
+ }
+
+ if (user == args.Target && !_config.GetCVar(CCVars.CanOperateOnSelf))
+ {
+ _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);
+ }
+
+ private void OnSurgeryStepDamage(Entity ent, ref SurgeryStepDamageEvent args) =>
+ SetDamage(args.Body, args.Damage, args.PartMultiplier, args.User, args.Part);
+
+ private void OnSurgeryDamageChange(Entity ent, ref SurgeryStepEvent args)
+ {
+ // This unintentionally punishes the user if they have an organ in another hand that is already used.
+ // Imo surgery shouldn't let you automatically pick tools on both hands anyway, it should only use the one you've got in your selected hand.
+ if (ent.Comp.IsConsumable
+ && args.Tools.Where(tool => TryComp(tool, out var organComp)
+ && !_body.TrySetOrganUsed(tool, true, organComp)).Any())
+ return;
+
+ var damageChange = ent.Comp.Damage;
+ if (HasComp(args.Body))
+ damageChange = damageChange * ent.Comp.SleepModifier;
+
+ SetDamage(args.Body, damageChange, 0.5f, args.User, args.Part);
+ }
+
+ private void OnSurgerySpecialDamageChange(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (ent.Comp.IsConsumable
+ && args.Tools.Where(tool => TryComp(tool, out var organComp)
+ && !_body.TrySetOrganUsed(tool, true, organComp)).Any())
+ return;
+
+ if (ent.Comp.DamageType == "Rot")
+ _rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); // BEHOLD, SHITCODE THAT I JUST COPY PASTED. I'll redo it at some point, pinky swear :)
+ else if (ent.Comp.DamageType == "Eye"
+ && TryComp(args.Body, out BlindableComponent? blindComp)
+ && blindComp.EyeDamage > 0)
+ _blindableSystem.AdjustEyeDamage((args.Body, blindComp), -blindComp!.EyeDamage);
+ }
+
+ private void OnStepScreamComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (HasComp(args.Body))
+ return;
+
+ _chat.TryEmoteWithChat(args.Body, ent.Comp.Emote);
+ }
+ private void OnStepSpawnComplete(Entity ent, ref SurgeryStepEvent args) =>
+ SpawnAtPosition(ent.Comp.Entity, Transform(args.Body).Coordinates);
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ {
+ if (!args.WasModified())
+ return;
+
+ LoadPrototypes();
+ }
+
+ private void LoadPrototypes()
+ {
+ _surgeries.Clear();
+ foreach (var entity in _prototypes.EnumeratePrototypes())
+ if (entity.HasComponent())
+ _surgeries.Add(new EntProtoId(entity.ID));
+ }
+}
diff --git a/Content.Server/Targeting/TargetingSystem.cs b/Content.Server/Targeting/TargetingSystem.cs
new file mode 100644
index 00000000000..3fc8ea59640
--- /dev/null
+++ b/Content.Server/Targeting/TargetingSystem.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Body.Systems;
+using Content.Shared.Mobs;
+using Content.Shared.Targeting;
+using Content.Shared.Targeting.Events;
+
+namespace Content.Server.Targeting;
+public sealed class TargetingSystem : SharedTargetingSystem
+{
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(OnTargetChange);
+ SubscribeLocalEvent(OnMobStateChange);
+ }
+
+ private void OnTargetChange(TargetChangeEvent message, EntitySessionEventArgs args)
+ {
+ if (!TryComp(GetEntity(message.Uid), out var target))
+ return;
+
+ target.Target = message.BodyPart;
+ Dirty(GetEntity(message.Uid), target);
+ }
+
+ private void OnMobStateChange(EntityUid uid, TargetingComponent component, MobStateChangedEvent args)
+ {
+ // Revival is handled by the server, so we're keeping all of this here.
+ var changed = false;
+
+ if (args.NewMobState == MobState.Dead)
+ {
+ foreach (var part in GetValidParts())
+ {
+ component.BodyStatus[part] = TargetIntegrity.Dead;
+ changed = true;
+ }
+ // I love groin shitcode.
+ component.BodyStatus[TargetBodyPart.Groin] = TargetIntegrity.Dead;
+ }
+ else if (args.OldMobState == MobState.Dead && (args.NewMobState == MobState.Alive || args.NewMobState == MobState.Critical))
+ {
+ component.BodyStatus = _bodySystem.GetBodyPartStatus(uid);
+ changed = true;
+ }
+
+ if (changed)
+ {
+ Dirty(uid, component);
+ RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(uid)), uid);
+ }
+ }
+}
diff --git a/Content.Server/Traits/Assorted/LightweightDrunkSystem.cs b/Content.Server/Traits/Assorted/LightweightDrunkSystem.cs
index b5e9b877764..f974fe75607 100644
--- a/Content.Server/Traits/Assorted/LightweightDrunkSystem.cs
+++ b/Content.Server/Traits/Assorted/LightweightDrunkSystem.cs
@@ -13,7 +13,7 @@ public override void Initialize()
private void OnTryMetabolizeReagent(EntityUid uid, LightweightDrunkComponent comp, ref TryMetabolizeReagent args)
{
- Log.Debug(args.Prototype.ID);
+ //Log.Debug(args.Prototype.ID);
if (args.Prototype.ID != "Ethanol")
return;
diff --git a/Content.Shared/Body/Events/AmputateAttemptEvent.cs b/Content.Shared/Body/Events/AmputateAttemptEvent.cs
new file mode 100644
index 00000000000..b71a0407bf2
--- /dev/null
+++ b/Content.Shared/Body/Events/AmputateAttemptEvent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Body.Events;
+
+///
+/// Raised on an entity when attempting to remove a body part.
+///
+[ByRefEvent]
+public readonly record struct AmputateAttemptEvent(EntityUid Part);
diff --git a/Content.Shared/Body/Organ/DebrainedComponent.cs b/Content.Shared/Body/Organ/DebrainedComponent.cs
new file mode 100644
index 00000000000..12574bddcc3
--- /dev/null
+++ b/Content.Shared/Body/Organ/DebrainedComponent.cs
@@ -0,0 +1,7 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class DebrainedComponent : Component;
+// TODO: Add a timer to kill the entity if they don't get a new brain in time.
diff --git a/Content.Shared/Body/Organ/EarsComponent.cs b/Content.Shared/Body/Organ/EarsComponent.cs
new file mode 100644
index 00000000000..80414387292
--- /dev/null
+++ b/Content.Shared/Body/Organ/EarsComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class EarsComponent : Component;
diff --git a/Content.Shared/Body/Organ/EyesComponent.cs b/Content.Shared/Body/Organ/EyesComponent.cs
new file mode 100644
index 00000000000..55be5f1a9c4
--- /dev/null
+++ b/Content.Shared/Body/Organ/EyesComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class EyesComponent : Component;
diff --git a/Content.Shared/Body/Organ/HeartComponent.cs b/Content.Shared/Body/Organ/HeartComponent.cs
new file mode 100644
index 00000000000..fc4def945eb
--- /dev/null
+++ b/Content.Shared/Body/Organ/HeartComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class HeartComponent : Component;
diff --git a/Content.Shared/Body/Organ/LiverComponent.cs b/Content.Shared/Body/Organ/LiverComponent.cs
new file mode 100644
index 00000000000..23021bea319
--- /dev/null
+++ b/Content.Shared/Body/Organ/LiverComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class LiverComponent : Component;
diff --git a/Content.Shared/Body/Organ/MarkingContainerComponent.cs b/Content.Shared/Body/Organ/MarkingContainerComponent.cs
new file mode 100644
index 00000000000..0583258dc20
--- /dev/null
+++ b/Content.Shared/Body/Organ/MarkingContainerComponent.cs
@@ -0,0 +1,15 @@
+// This is a uh, very shitty copout to not wanting to modify the prototypes for felinids, and entities at large so they have ears.
+// I will do that at some point, for now I just want the funny surgery to work lol.
+using Robust.Shared.GameStates;
+using Content.Shared.Humanoid.Markings;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class MarkingContainerComponent : Component
+{
+ [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string Marking = default!;
+
+}
diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs
index 3048927b5fb..c7212cbec31 100644
--- a/Content.Shared/Body/Organ/OrganComponent.cs
+++ b/Content.Shared/Body/Organ/OrganComponent.cs
@@ -1,16 +1,34 @@
using Content.Shared.Body.Systems;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
+using Content.Shared.Medical.Surgery.Tools;
namespace Content.Shared.Body.Organ;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedBodySystem))]
-public sealed partial class OrganComponent : Component
+public sealed partial class OrganComponent : Component, ISurgeryToolComponent
{
///
/// Relevant body this organ is attached to.
///
[DataField, AutoNetworkedField]
public EntityUid? Body;
+
+ ///
+ /// Shitcodey solution to not being able to know what name corresponds to each organ's slot ID
+ /// without referencing the prototype or hardcoding.
+ ///
+
+ [DataField]
+ public string SlotId = "";
+
+ [DataField]
+ public string ToolName { get; set; } = "An organ";
+
+ ///
+ /// If true, the organ will not heal an entity when transplanted into them.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool? Used { get; set; }
}
diff --git a/Content.Shared/Body/Organ/TailComponent.cs b/Content.Shared/Body/Organ/TailComponent.cs
new file mode 100644
index 00000000000..3cd8da87b5a
--- /dev/null
+++ b/Content.Shared/Body/Organ/TailComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Organ;
+
+[RegisterComponent]
+public sealed partial class TailComponent : Component;
diff --git a/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs
new file mode 100644
index 00000000000..1769d68ec76
--- /dev/null
+++ b/Content.Shared/Body/Part/BodyPartAppearanceComponent.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Humanoid.Markings;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body.Part;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class BodyPartAppearanceComponent : Component
+{
+ ///
+ /// HumanoidVisualLayer type for this body part.
+ ///
+ [DataField, AutoNetworkedField]
+ public HumanoidVisualLayers Type { get; set; }
+
+ ///
+ /// Relevant markings for this body part that will be applied on attachment.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary> Markings = new();
+
+ ///
+ /// ID of this custom base layer. Must be a .
+ /// I don't actually know if these serializer props are necessary. I just lifted this from MS14 lol.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), AutoNetworkedField]
+ public string? ID { get; set; }
+
+ ///
+ /// Color of this custom base layer. Null implies skin colour if the corresponding is set to match skin.
+ ///
+ [DataField, AutoNetworkedField]
+ public Color? Color { get; set; }
+
+ ///
+ /// Color of this custom base eye layer. Null implies eye colour if the corresponding is set to match skin.
+ ///
+ [DataField, AutoNetworkedField]
+ public Color? EyeColor { get; set; }
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? OriginalBody { get; set; }
+}
diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs
index c4e65c06a3f..2a93f6aed25 100644
--- a/Content.Shared/Body/Part/BodyPartComponent.cs
+++ b/Content.Shared/Body/Part/BodyPartComponent.cs
@@ -1,5 +1,10 @@
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Content.Shared.Medical.Surgery.Tools;
+using Content.Shared.Targeting;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -8,7 +13,7 @@ namespace Content.Shared.Body.Part;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedBodySystem))]
-public sealed partial class BodyPartComponent : Component
+public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent
{
// Need to set this on container changes as it may be several transform parents up the hierarchy.
///
@@ -17,6 +22,12 @@ public sealed partial class BodyPartComponent : Component
[DataField, AutoNetworkedField]
public EntityUid? Body;
+ [DataField, AutoNetworkedField]
+ public EntityUid? OriginalBody;
+
+ [DataField, AutoNetworkedField]
+ public BodyPartSlot? ParentSlot;
+
[DataField, AutoNetworkedField]
public BodyPartType PartType = BodyPartType.Other;
@@ -28,9 +39,23 @@ public sealed partial class BodyPartComponent : Component
[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 = 100;
+
+
[DataField, AutoNetworkedField]
public BodyPartSymmetry Symmetry = BodyPartSymmetry.None;
+ [DataField]
+ public string ToolName { get; set; } = "A body part";
+
+ [DataField, AutoNetworkedField]
+ public bool? Used { get; set; } = null;
+
///
/// Child body parts attached to this body part.
///
@@ -43,6 +68,90 @@ public sealed partial class BodyPartComponent : Component
[DataField, AutoNetworkedField]
public Dictionary Organs = new();
+ ///
+ /// What's the max health this body part can have?
+ ///
+ [DataField]
+ public float MinIntegrity;
+
+ ///
+ /// Whether this body part is enabled or not.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Enabled = true;
+
+ ///
+ /// Whether this body part can be enabled or not. Used for non-functional prosthetics.
+ ///
+ [DataField]
+ public bool CanEnable = true;
+
+ ///
+ /// How long it takes to run another self heal tick on the body part.
+ ///
+ [DataField]
+ public float HealingTime = 30;
+
+ ///
+ /// How long it has been since the last self heal tick on the body part.
+ ///
+ public float HealingTimer;
+
+ ///
+ /// How much health to heal on the body part per tick.
+ ///
+ [DataField]
+ public float SelfHealingAmount = 5;
+
+ ///
+ /// The name of the container for this body part. Used in insertion surgeries.
+ ///
+ [DataField]
+ public string ContainerName { get; set; } = "part_slot";
+
+ ///
+ /// The slot for item insertion.
+ ///
+ [DataField, AutoNetworkedField]
+ public ItemSlot ItemInsertionSlot = new();
+
+
+ ///
+ /// Current species. Dictates things like body part sprites.
+ ///
+ [DataField, AutoNetworkedField]
+ public string Species { get; set; } = "";
+
+ ///
+ /// The total damage that has to be dealt to a body part
+ /// to make possible severing it.
+ ///
+ [DataField, AutoNetworkedField]
+ public float SeverIntegrity = 90;
+
+ ///
+ /// The ID of the base layer for this body part.
+ ///
+ [DataField, AutoNetworkedField]
+ public string? BaseLayerId;
+
+ ///
+ /// On what TargetIntegrity we should re-enable the part.
+ ///
+ [DataField, AutoNetworkedField]
+ public TargetIntegrity EnableIntegrity = TargetIntegrity.ModeratelyWounded;
+
+ [DataField, AutoNetworkedField]
+ public Dictionary IntegrityThresholds = new()
+ {
+ { TargetIntegrity.CriticallyWounded, 90 },
+ { TargetIntegrity.HeavilyWounded, 75 },
+ { TargetIntegrity.ModeratelyWounded, 60 },
+ { TargetIntegrity.SomewhatWounded, 40},
+ { TargetIntegrity.LightlyWounded, 20 },
+ { TargetIntegrity.Healthy, 10 },
+ };
+
///
/// These are only for VV/Debug do not use these for gameplay/systems
///
diff --git a/Content.Shared/Body/Part/BodyPartEvents.cs b/Content.Shared/Body/Part/BodyPartEvents.cs
index 0d8d2c8a268..9872b092002 100644
--- a/Content.Shared/Body/Part/BodyPartEvents.cs
+++ b/Content.Shared/Body/Part/BodyPartEvents.cs
@@ -1,7 +1,27 @@
+using Content.Shared.Humanoid;
+
namespace Content.Shared.Body.Part;
[ByRefEvent]
public readonly record struct BodyPartAddedEvent(string Slot, Entity Part);
+// Kind of a clone of the above for surgical reattachment specifically.
+[ByRefEvent]
+public readonly record struct BodyPartAttachedEvent(Entity Part);
+
[ByRefEvent]
public readonly record struct BodyPartRemovedEvent(string Slot, Entity Part);
+
+// Kind of a clone of the above for any instances where we call DropPart(), reasoning being that RemovedEvent fires off
+// a lot more often than what I'd like due to PVS.
+[ByRefEvent]
+public readonly record struct BodyPartDroppedEvent(Entity Part);
+
+[ByRefEvent]
+public readonly record struct BodyPartEnableChangedEvent(bool Enabled);
+
+[ByRefEvent]
+public readonly record struct BodyPartEnabledEvent(Entity Part);
+
+[ByRefEvent]
+public readonly record struct BodyPartDisabledEvent(Entity Part);
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
index 1a35afdbe00..f309cc238c6 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs
@@ -4,17 +4,25 @@
using Content.Shared.Body.Organ;
using Content.Shared.Body.Part;
using Content.Shared.Body.Prototypes;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Damage;
using Content.Shared.DragDrop;
+using Content.Shared.FixedPoint;
using Content.Shared.Gibbing.Components;
using Content.Shared.Gibbing.Events;
using Content.Shared.Gibbing.Systems;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Events;
using Content.Shared.Inventory;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Standing;
+using Content.Shared.Targeting;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Utility;
-
+using Robust.Shared.Timing;
namespace Content.Shared.Body.Systems;
public partial class SharedBodySystem
@@ -27,9 +35,10 @@ public partial class SharedBodySystem
*/
[Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly ItemSlotsSystem _slots = default!;
[Dependency] private readonly GibbingSystem _gibbingSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
-
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
private const float GibletLaunchImpulse = 8;
private const float GibletLaunchImpulseVariance = 3;
@@ -42,6 +51,8 @@ private void InitializeBody()
SubscribeLocalEvent(OnBodyInit);
SubscribeLocalEvent(OnBodyMapInit);
SubscribeLocalEvent(OnBodyCanDrag);
+ SubscribeLocalEvent(OnStandAttempt);
+ SubscribeLocalEvent(OnProfileLoadFinished);
}
private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args)
@@ -115,11 +126,11 @@ private void MapInitBody(EntityUid bodyEntity, BodyPrototype prototype)
var rootPartUid = SpawnInContainerOrDrop(protoRoot.Part, bodyEntity, BodyRootContainerId);
var rootPart = Comp(rootPartUid);
rootPart.Body = bodyEntity;
+ rootPart.OriginalBody = bodyEntity;
Dirty(rootPartUid, rootPart);
-
// Setup the rest of the body entities.
SetupOrgans((rootPartUid, rootPart), protoRoot.Organs);
- MapInitParts(rootPartUid, prototype);
+ MapInitParts(rootPartUid, rootPart, prototype);
}
private void OnBodyCanDrag(Entity ent, ref CanDragEvent args)
@@ -127,10 +138,16 @@ private void OnBodyCanDrag(Entity ent, ref CanDragEvent args)
args.Handled = true;
}
+ private void OnStandAttempt(Entity ent, ref StandAttemptEvent args)
+ {
+ if (ent.Comp.LegEntities.Count == 0)
+ args.Cancel();
+ }
+
///
/// Sets up all of the relevant body parts for a particular body entity and root part.
///
- private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype)
+ private void MapInitParts(EntityUid rootPartId, BodyPartComponent rootPart, BodyPrototype prototype)
{
// Start at the root part and traverse the body graph, setting up parts as we go.
// Basic BFS pathfind.
@@ -168,6 +185,9 @@ private void MapInitParts(EntityUid rootPartId, BodyPrototype prototype)
var childPartComponent = Comp(childPart);
var partSlot = CreatePartSlot(parentEntity, connection, childPartComponent.PartType, parentPartComponent);
+ childPartComponent.ParentSlot = partSlot;
+ childPartComponent.OriginalBody = rootPart.Body;
+ Dirty(childPart, childPartComponent);
var cont = Containers.GetContainer(parentEntity, GetPartSlotContainerId(connection));
if (partSlot is null || !Containers.Insert(childPart, cont))
@@ -233,11 +253,11 @@ public IEnumerable GetBodyContainers(
{
if (id is null
|| !Resolve(id.Value, ref body, logMissing: false)
+ || body is null
+ || body.RootContainer == default
|| body.RootContainer.ContainedEntity is null
|| !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart))
- {
yield break;
- }
foreach (var child in GetBodyPartChildren(body.RootContainer.ContainedEntity.Value, rootPart))
{
@@ -291,7 +311,9 @@ public virtual HashSet GibBody(
Vector2? splatDirection = null,
float splatModifier = 1,
Angle splatCone = default,
- SoundSpecifier? gibSoundOverride = null)
+ SoundSpecifier? gibSoundOverride = null,
+ GibType gib = GibType.Gib,
+ GibContentsOption contents = GibContentsOption.Drop)
{
var gibs = new HashSet();
@@ -308,9 +330,9 @@ public virtual HashSet GibBody(
foreach (var part in parts)
{
- _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs,
- playAudio: false, launchGibs:true, launchDirection:splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier,
- launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
+ _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, gib, contents, ref gibs,
+ playAudio: false, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier,
+ launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone);
if (!gibOrgans)
continue;
@@ -319,7 +341,7 @@ public virtual HashSet GibBody(
{
_gibbingSystem.TryGibEntityWithRef(bodyId, organ.Id, GibType.Drop, GibContentsOption.Skip,
ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier,
- launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
+ launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone);
}
}
if (TryComp(bodyId, out var inventory))
@@ -333,4 +355,57 @@ public virtual HashSet GibBody(
_audioSystem.PlayPredicted(gibSoundOverride, Transform(bodyId).Coordinates, null);
return gibs;
}
+
+ public virtual HashSet GibPart(
+ EntityUid partId,
+ BodyPartComponent? part = null,
+ bool launchGibs = true,
+ Vector2? splatDirection = null,
+ float splatModifier = 1,
+ Angle splatCone = default,
+ SoundSpecifier? gibSoundOverride = null)
+ {
+ var gibs = new HashSet();
+
+ if (!Resolve(partId, ref part, logMissing: false))
+ return gibs;
+
+ if (part.Body is { } bodyEnt)
+ {
+ RemovePartChildren((partId, part), bodyEnt);
+ foreach (var organ in GetPartOrgans(partId, part))
+ {
+ _gibbingSystem.TryGibEntityWithRef(bodyEnt, organ.Id, GibType.Drop, GibContentsOption.Skip,
+ ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier,
+ launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone);
+ }
+ var ev = new BodyPartDroppedEvent((partId, part));
+ RaiseLocalEvent(bodyEnt, ref ev);
+ }
+
+ _gibbingSystem.TryGibEntityWithRef(partId, partId, GibType.Gib, GibContentsOption.Drop, ref gibs,
+ playAudio: true, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier,
+ launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone);
+
+
+ if (HasComp(partId))
+ {
+ foreach (var item in _inventory.GetHandOrInventoryEntities(partId))
+ {
+ SharedTransform.AttachToGridOrMap(item);
+ gibs.Add(item);
+ }
+ }
+ _audioSystem.PlayPredicted(gibSoundOverride, Transform(partId).Coordinates, null);
+ return gibs;
+ }
+
+ private void OnProfileLoadFinished(EntityUid uid, BodyComponent component, ProfileLoadFinishedEvent args)
+ {
+ if (!HasComp(uid)
+ || TerminatingOrDeleted(uid))
+
+ foreach (var part in GetBodyChildren(uid, component))
+ EnsureComp(part.Id);
+ }
}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs
index efabebfc858..e006d0feeb2 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Organs.cs
@@ -212,4 +212,15 @@ public bool TryGetBodyOrganComponents(
comps = null;
return false;
}
+
+ public bool TrySetOrganUsed(EntityUid organId, bool used, OrganComponent? organ = null)
+ {
+ if (!Resolve(organId, ref organ)
+ || organ.Used == true)
+ return false;
+
+ organ.Used = true;
+ Dirty(organId, organ);
+ return true;
+ }
}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs
new file mode 100644
index 00000000000..50b9fb8c07e
--- /dev/null
+++ b/Content.Shared/Body/Systems/SharedBodySystem.PartAppearance.cs
@@ -0,0 +1,198 @@
+using System.Diagnostics;
+using System.Linq;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Body.Systems;
+public partial class SharedBodySystem
+{
+ [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
+ [Dependency] private readonly MarkingManager _markingManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ private void InitializePartAppearances()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnPartAppearanceStartup);
+ SubscribeLocalEvent(HandleState);
+ SubscribeLocalEvent(OnPartAttachedToBody);
+ SubscribeLocalEvent(OnPartDroppedFromBody);
+ }
+
+ private void OnPartAppearanceStartup(EntityUid uid, BodyPartAppearanceComponent component, ComponentStartup args)
+ {
+ if (!TryComp(uid, out BodyPartComponent? part))
+ return;
+
+ if (part.OriginalBody == null
+ || TerminatingOrDeleted(part.OriginalBody.Value)
+ || !TryComp(part.OriginalBody.Value, out HumanoidAppearanceComponent? bodyAppearance)
+ || part.ToHumanoidLayers() is not { } relevantLayer)
+ {
+ component.ID = part.BaseLayerId;
+ return;
+ }
+
+ var customLayers = bodyAppearance.CustomBaseLayers;
+ var spriteLayers = bodyAppearance.BaseLayers;
+ component.Type = relevantLayer;
+ component.OriginalBody = part.OriginalBody.Value;
+
+ part.Species = bodyAppearance.Species;
+
+ if (customLayers.ContainsKey(component.Type))
+ {
+ component.ID = customLayers[component.Type].Id;
+ component.Color = customLayers[component.Type].Color;
+ }
+ else if (spriteLayers.ContainsKey(component.Type))
+ {
+ component.ID = spriteLayers[component.Type].ID;
+ component.Color = bodyAppearance.SkinColor;
+ }
+ else
+ {
+ component.ID = CreateIdFromPart(bodyAppearance, relevantLayer);
+ component.Color = bodyAppearance.SkinColor;
+ }
+
+ // I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS I HATE HARDCODED CHECKS
+ if (part.PartType == BodyPartType.Head)
+ component.EyeColor = bodyAppearance.EyeColor;
+
+ var markingsByLayer = new Dictionary>();
+
+ foreach (var layer in HumanoidVisualLayersExtension.Sublayers(relevantLayer))
+ {
+ var category = MarkingCategoriesConversion.FromHumanoidVisualLayers(layer);
+ if (bodyAppearance.MarkingSet.Markings.TryGetValue(category, out var markingList))
+ markingsByLayer[layer] = markingList.Select(m => new Marking(m.MarkingId, m.MarkingColors.ToList())).ToList();
+ }
+
+ component.Markings = markingsByLayer;
+ }
+
+ private string? CreateIdFromPart(HumanoidAppearanceComponent bodyAppearance, HumanoidVisualLayers part)
+ {
+ var speciesProto = _prototypeManager.Index(bodyAppearance.Species);
+ var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
+
+ if (!baseSprites.Sprites.ContainsKey(part))
+ return null;
+
+ return HumanoidVisualLayersExtension.GetSexMorph(part, bodyAppearance.Sex, baseSprites.Sprites[part]);
+ }
+
+ public void ModifyMarkings(EntityUid uid,
+ Entity partAppearance,
+ HumanoidAppearanceComponent bodyAppearance,
+ HumanoidVisualLayers targetLayer,
+ string markingId,
+ bool remove = false)
+ {
+
+ if (!Resolve(partAppearance, ref partAppearance.Comp))
+ return;
+
+ if (!remove)
+ {
+
+ if (!_markingManager.Markings.TryGetValue(markingId, out var prototype))
+ return;
+
+ var markingColors = MarkingColoring.GetMarkingLayerColors(
+ prototype,
+ bodyAppearance.SkinColor,
+ bodyAppearance.EyeColor,
+ bodyAppearance.MarkingSet
+ );
+
+ var marking = new Marking(markingId, markingColors);
+
+ _humanoid.SetLayerVisibility(uid, targetLayer, true, true, bodyAppearance);
+ _humanoid.AddMarking(uid, markingId, markingColors, true, true, bodyAppearance);
+ if (!partAppearance.Comp.Markings.ContainsKey(targetLayer))
+ partAppearance.Comp.Markings[targetLayer] = new List();
+
+ partAppearance.Comp.Markings[targetLayer].Add(marking);
+ }
+ //else
+ //RemovePartMarkings(uid, component, bodyAppearance);
+ }
+
+ private void HandleState(EntityUid uid, BodyPartAppearanceComponent component, ref AfterAutoHandleStateEvent args) =>
+ ApplyPartMarkings(uid, component);
+
+ private void OnPartAttachedToBody(EntityUid uid, BodyComponent component, ref BodyPartAttachedEvent args)
+ {
+ if (!TryComp(args.Part, out BodyPartAppearanceComponent? partAppearance)
+ || !TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance))
+ return;
+
+ if (partAppearance.ID != null)
+ _humanoid.SetBaseLayerId(uid, partAppearance.Type, partAppearance.ID, sync: true, bodyAppearance);
+
+ UpdateAppearance(uid, partAppearance);
+ }
+
+ private void OnPartDroppedFromBody(EntityUid uid, BodyComponent component, ref BodyPartDroppedEvent args)
+ {
+ if (TerminatingOrDeleted(uid)
+ || TerminatingOrDeleted(args.Part)
+ || !TryComp(uid, out HumanoidAppearanceComponent? bodyAppearance))
+ return;
+
+ // We check for this conditional here since some entities may not have a profile... If they dont
+ // have one, and their part is gibbed, the markings will not be removed or applied properly.
+ if (!HasComp(args.Part))
+ EnsureComp(args.Part);
+
+ if (TryComp(args.Part, out var partAppearance))
+ RemoveAppearance(uid, partAppearance, args.Part);
+ }
+
+ protected void UpdateAppearance(EntityUid target,
+ BodyPartAppearanceComponent component)
+ {
+ if (!TryComp(target, out HumanoidAppearanceComponent? bodyAppearance))
+ return;
+
+ if (component.EyeColor != null)
+ bodyAppearance.EyeColor = component.EyeColor.Value;
+
+ if (component.Color != null)
+ _humanoid.SetBaseLayerColor(target, component.Type, component.Color, true, bodyAppearance);
+
+ _humanoid.SetLayerVisibility(target, component.Type, true, true, bodyAppearance);
+
+ foreach (var (visualLayer, markingList) in component.Markings)
+ {
+ _humanoid.SetLayerVisibility(target, visualLayer, true, true, bodyAppearance);
+ foreach (var marking in markingList)
+ _humanoid.AddMarking(target, marking.MarkingId, marking.MarkingColors, false, true, bodyAppearance);
+ }
+
+ Dirty(target, bodyAppearance);
+ }
+
+ protected void RemoveAppearance(EntityUid entity, BodyPartAppearanceComponent component, EntityUid partEntity)
+ {
+ if (!TryComp(entity, out HumanoidAppearanceComponent? bodyAppearance))
+ return;
+
+ foreach (var (visualLayer, markingList) in component.Markings)
+ {
+ _humanoid.SetLayerVisibility(entity, visualLayer, false, true, bodyAppearance);
+ }
+ RemoveBodyMarkings(entity, component, bodyAppearance);
+ }
+
+ protected abstract void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component);
+
+ protected abstract void RemoveBodyMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance);
+}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
index ee79faa0b8e..be03fa6f404 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
@@ -1,28 +1,55 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Body.Part;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
+using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
using Content.Shared.Movement.Components;
+using Content.Shared.Random;
+using Content.Shared.Targeting.Events;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
namespace Content.Shared.Body.Systems;
public partial class SharedBodySystem
{
+ [Dependency] private readonly RandomHelperSystem _randomHelper = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
private void InitializeParts()
{
// TODO: This doesn't handle comp removal on child ents.
// If you modify this also see the Body partial for root parts.
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnBodyPartRemove);
SubscribeLocalEvent(OnBodyPartInserted);
SubscribeLocalEvent(OnBodyPartRemoved);
+ SubscribeLocalEvent(OnAmputateAttempt);
+ SubscribeLocalEvent(OnPartEnableChanged);
}
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (ent.Comp.PartType == BodyPartType.Torso)
+ {
+ // For whatever reason this slot is initialized properly on the server, but not on the client.
+ // This seems to be an issue due to wiz-merge, on my old branch it was properly instantiating
+ // ItemInsertionSlot's container on both ends. It does show up properly on ItemSlotsComponent though.
+ _slots.AddItemSlot(ent, ent.Comp.ContainerName, ent.Comp.ItemInsertionSlot);
+ Dirty(ent, ent.Comp);
+ }
+ }
+
+ private void OnBodyPartRemove(Entity ent, ref ComponentRemove args)
+ {
+ if (ent.Comp.PartType == BodyPartType.Torso)
+ _slots.RemoveItemSlot(ent, ent.Comp.ItemInsertionSlot);
+ }
private void OnBodyPartInserted(Entity ent, ref EntInsertedIntoContainerMessage args)
{
// Body part inserted into another body part.
@@ -47,12 +74,12 @@ private void OnBodyPartRemoved(Entity ent, ref EntRemovedFrom
// Body part removed from another body part.
var removedUid = args.Entity;
var slotId = args.Container.ID;
-
DebugTools.Assert(!TryComp(removedUid, out BodyPartComponent? b) || b.Body == ent.Comp.Body);
DebugTools.Assert(!TryComp(removedUid, out OrganComponent? o) || o.Body == ent.Comp.Body);
if (TryComp(removedUid, out BodyPartComponent? part) && part.Body is not null)
{
+ CheckBodyPart((removedUid, part), GetTargetBodyPart(part), true);
RemovePart(part.Body.Value, (removedUid, part), slotId);
RecursiveBodyUpdate((removedUid, part), null);
}
@@ -93,6 +120,8 @@ private void RecursiveBodyUpdate(Entity ent, EntityUid? bodyU
}
}
+ // The code for RemovePartEffect() should live here, because it literally is the point of this recursive function.
+ // But the debug asserts at the top plus existing tests need refactoring for this. So we'll be lazy.
foreach (var slotId in ent.Comp.Children.Keys)
{
if (!Containers.TryGetContainer(ent, GetPartSlotContainerId(slotId), out var container))
@@ -116,7 +145,6 @@ protected virtual void AddPart(
var ev = new BodyPartAddedEvent(slotId, partEnt);
RaiseLocalEvent(bodyEnt, ref ev);
-
AddLeg(partEnt, bodyEnt);
}
@@ -127,15 +155,37 @@ protected virtual void RemovePart(
{
Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false);
Dirty(partEnt, partEnt.Comp);
- partEnt.Comp.Body = null;
+ partEnt.Comp.ParentSlot = null;
+ partEnt.Comp.OriginalBody = partEnt.Comp.Body;
var ev = new BodyPartRemovedEvent(slotId, partEnt);
RaiseLocalEvent(bodyEnt, ref ev);
-
RemoveLeg(partEnt, bodyEnt);
+ RemovePartEffect(partEnt, bodyEnt);
PartRemoveDamage(bodyEnt, partEnt);
}
+ protected virtual void DropPart(Entity partEnt)
+ {
+ ChangeSlotState(partEnt, true);
+ // I don't know if this can cause issues, since any part that's being detached HAS to have a Body.
+ // though I really just want the compiler to shut the fuck up.
+ var body = partEnt.Comp.Body.GetValueOrDefault();
+ if (TryComp(partEnt, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted)
+ {
+ var enableEvent = new BodyPartEnableChangedEvent(false);
+ RaiseLocalEvent(partEnt, ref enableEvent);
+ var droppedEvent = new BodyPartDroppedEvent(partEnt);
+ RaiseLocalEvent(body, ref droppedEvent);
+ SharedTransform.AttachToGridOrMap(partEnt, transform);
+ _randomHelper.RandomOffset(partEnt, 0.5f);
+ }
+
+ }
+
+ private void OnAmputateAttempt(Entity partEnt, ref AmputateAttemptEvent args) =>
+ DropPart(partEnt);
+
private void AddLeg(Entity legEnt, Entity bodyEnt)
{
if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false))
@@ -159,11 +209,41 @@ private void RemoveLeg(Entity legEnt, Entity
bodyEnt.Comp.LegEntities.Remove(legEnt);
UpdateMovementSpeed(bodyEnt);
Dirty(bodyEnt, bodyEnt.Comp);
+ Standing.Down(bodyEnt);
+ }
+ }
+
+ // TODO: Refactor this crap.
+ private void RemovePartEffect(Entity partEnt, Entity bodyEnt)
+ {
+ if (TerminatingOrDeleted(bodyEnt)
+ || !Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false))
+ return;
+
+ RemovePartChildren(partEnt, bodyEnt, bodyEnt.Comp);
+ }
+
+ protected void RemovePartChildren(Entity partEnt, EntityUid bodyEnt, BodyComponent? body = null)
+ {
+ if (!Resolve(bodyEnt, ref body, logMissing: false))
+ return;
- if (!bodyEnt.Comp.LegEntities.Any())
+ if (partEnt.Comp.Children.Any())
+ {
+ foreach (var slotId in partEnt.Comp.Children.Keys)
{
- Standing.Down(bodyEnt);
+ if (Containers.TryGetContainer(partEnt, GetPartSlotContainerId(slotId), out var container) &&
+ container is ContainerSlot slot &&
+ slot.ContainedEntity is { } childEntity &&
+ TryComp(childEntity, out BodyPartComponent? childPart))
+ {
+ var ev = new BodyPartEnableChangedEvent(false);
+ RaiseLocalEvent(childEntity, ref ev);
+ DropPart((childEntity, childPart));
+ }
}
+
+ Dirty(bodyEnt, body);
}
}
@@ -177,9 +257,94 @@ 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);
+ }
+ }
+
+ private void OnPartEnableChanged(Entity partEnt, ref BodyPartEnableChangedEvent args)
+ {
+ if (!partEnt.Comp.CanEnable && args.Enabled)
+ return;
+
+ partEnt.Comp.Enabled = args.Enabled;
+ Dirty(partEnt, partEnt.Comp);
+
+ if (args.Enabled)
+ EnablePart(partEnt);
+ else
+ DisablePart(partEnt);
+ }
+
+ private void EnablePart(Entity partEnt)
+ {
+ if (!TryComp(partEnt.Comp.Body, out BodyComponent? body))
+ return;
+
+ // I hate having to hardcode these checks so much.
+ if (partEnt.Comp.PartType == BodyPartType.Leg)
+ AddLeg(partEnt, (partEnt.Comp.Body.Value, body));
+
+ if (partEnt.Comp.PartType == BodyPartType.Arm)
+ {
+ var hand = GetBodyChildrenOfType(partEnt.Comp.Body.Value, BodyPartType.Hand, symmetry: partEnt.Comp.Symmetry).FirstOrDefault();
+ if (hand != default)
+ {
+ var ev = new BodyPartEnabledEvent(hand);
+ RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev);
+ }
+ }
+
+ if (partEnt.Comp.PartType == BodyPartType.Hand)
+ {
+ var ev = new BodyPartEnabledEvent(partEnt);
+ RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev);
+ }
+ }
+
+ ///
+ /// This function handles disabling or enabling equipment slots when an entity is
+ /// missing all of a given part type, or they get one added to them.
+ /// It is called right before dropping a part, or right after adding one.
+ ///
+ public void ChangeSlotState(Entity partEnt, bool disable)
+ {
+ if (partEnt.Comp.Body is not null
+ && GetBodyPartCount(partEnt.Comp.Body.Value, partEnt.Comp.PartType) == 1
+ && TryGetPartSlotContainerName(partEnt.Comp.PartType, out var containerNames))
+ {
+ foreach (var containerName in containerNames)
+ {
+ _inventorySystem.SetSlotStatus(partEnt.Comp.Body.Value, containerName, disable);
+ var ev = new RefreshInventorySlotsEvent(containerName);
+ RaiseLocalEvent(partEnt.Comp.Body.Value, ev);
+ }
+ }
+
+ }
+
+ private void DisablePart(Entity partEnt)
+ {
+ if (!TryComp(partEnt.Comp.Body, out BodyComponent? body))
+ return;
+
+ if (partEnt.Comp.PartType == BodyPartType.Leg)
+ RemoveLeg(partEnt, (partEnt.Comp.Body.Value, body));
+
+ if (partEnt.Comp.PartType == BodyPartType.Arm)
+ {
+ var hand = GetBodyChildrenOfType(partEnt.Comp.Body.Value, BodyPartType.Hand, symmetry: partEnt.Comp.Symmetry).FirstOrDefault();
+ if (hand != default)
+ {
+ var ev = new BodyPartDisabledEvent(hand);
+ RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev);
+ }
+ }
+
+ if (partEnt.Comp.PartType == BodyPartType.Hand)
+ {
+ var ev = new BodyPartDisabledEvent(partEnt);
+ RaiseLocalEvent(partEnt.Comp.Body.Value, ref ev);
}
}
@@ -438,12 +603,21 @@ public bool AttachPart(
return false;
}
+
if (!Containers.TryGetContainer(parentPartId, GetPartSlotContainerId(slot.Id), out var container))
{
DebugTools.Assert($"Unable to find body slot {slot.Id} for {ToPrettyString(parentPartId)}");
return false;
}
+ part.ParentSlot = slot;
+
+ if (TryComp(parentPart.Body, out HumanoidAppearanceComponent? bodyAppearance)
+ && !HasComp(partId)
+ && !TerminatingOrDeleted(parentPartId)
+ && !TerminatingOrDeleted(partId)) // Saw some exceptions involving these due to the spawn menu.
+ EnsureComp(partId);
+
return Containers.Insert(partId, container);
}
@@ -656,11 +830,12 @@ public bool BodyHasChild(
public IEnumerable<(EntityUid Id, BodyPartComponent Component)> GetBodyChildrenOfType(
EntityUid bodyId,
BodyPartType type,
- BodyComponent? body = null)
+ BodyComponent? body = null,
+ BodyPartSymmetry? symmetry = null)
{
foreach (var part in GetBodyChildren(bodyId, body))
{
- if (part.Component.PartType == type)
+ if (part.Component.PartType == type && (symmetry == null || part.Component.Symmetry == symmetry))
yield return part;
}
}
@@ -722,6 +897,49 @@ public bool TryGetBodyPartOrganComponents(
return false;
}
+ ///
+ /// Tries to get a list of ValueTuples of EntityUid and OrganComponent on each organ
+ /// in the given part.
+ ///
+ /// The part entity id to check on.
+ /// The type of component to check for.
+ /// The part to check for organs on.
+ /// The organs found on the body part.
+ /// Whether any were found.
+ ///
+ /// This method is somewhat of a copout to the fact that we can't use reflection to generically
+ /// get the type of component on runtime due to sandboxing. So we simply do a HasComp check for each organ.
+ ///
+ public bool TryGetBodyPartOrgans(
+ EntityUid uid,
+ Type type,
+ [NotNullWhen(true)] out List<(EntityUid Id, OrganComponent Organ)>? organs,
+ BodyPartComponent? part = null)
+ {
+ if (!Resolve(uid, ref part))
+ {
+ organs = null;
+ return false;
+ }
+
+ var list = new List<(EntityUid Id, OrganComponent Organ)>();
+
+ foreach (var organ in GetPartOrgans(uid, part))
+ {
+ if (HasComp(organ.Id, type))
+ list.Add((organ.Id, organ.Component));
+ }
+
+ if (list.Count != 0)
+ {
+ organs = list;
+ return true;
+ }
+
+ organs = null;
+ return false;
+ }
+
///
/// Gets the parent body part and all immediate child body parts for the partId.
///
@@ -790,5 +1008,31 @@ public bool TryGetBodyPartAdjacentPartsComponents(
return false;
}
+ private bool TryGetPartSlotContainerName(BodyPartType partType, out HashSet containerNames)
+ {
+ containerNames = partType switch
+ {
+ BodyPartType.Arm => new() { "gloves" },
+ BodyPartType.Leg => new() { "shoes" },
+ BodyPartType.Head => new() { "eyes", "ears", "head", "mask" },
+ _ => new()
+ };
+ return containerNames.Count > 0;
+ }
+
+ public int GetBodyPartCount(EntityUid bodyId, BodyPartType partType, BodyComponent? body = null)
+ {
+ if (!Resolve(bodyId, ref body, logMissing: false))
+ return 0;
+
+ int count = 0;
+ foreach (var part in GetBodyChildren(bodyId, body))
+ {
+ if (part.Component.PartType == partType)
+ count++;
+ }
+ return count;
+ }
+
#endregion
}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Targeting.cs b/Content.Shared/Body/Systems/SharedBodySystem.Targeting.cs
new file mode 100644
index 00000000000..9a7c9bc889e
--- /dev/null
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Targeting.cs
@@ -0,0 +1,503 @@
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Medical.Surgery.Steps.Parts;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+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.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Content.Shared.Body.Systems;
+
+public partial class SharedBodySystem
+{
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+
+ [Dependency] private readonly SharedPopupSystem _popup = 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();
+ SubscribeLocalEvent(OnBeforeDamageChanged);
+ SubscribeLocalEvent(OnBodyDamageModify);
+ SubscribeLocalEvent(OnPartDamageModify);
+ SubscribeLocalEvent(OnDamageChanged);
+ }
+
+ private void ProcessIntegrityTick(Entity entity)
+ {
+ if (!TryComp(entity, out var damageable))
+ return;
+
+ var damage = damageable.TotalDamage;
+
+ if (entity.Comp is { Body: { } body }
+ && damage > entity.Comp.MinIntegrity
+ && damage <= entity.Comp.IntegrityThresholds[TargetIntegrity.HeavilyWounded]
+ && _queryTargeting.HasComp(body)
+ && !_mobState.IsDead(body))
+ _damageable.TryChangeDamage(entity, GetHealingSpecifier(entity), canSever: false, targetPart: GetTargetBodyPart(entity));
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ _integrityJobQueue.Process();
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ using var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var ent, out var part))
+ {
+ part.HealingTimer += frameTime;
+
+ if (part.HealingTimer >= part.HealingTime)
+ {
+ part.HealingTimer = 0;
+ _integrityJobQueue.EnqueueJob(new IntegrityJob(this, (ent, part), IntegrityJobTime));
+ }
+ }
+ }
+
+ private void OnBeforeDamageChanged(Entity ent, ref BeforeDamageChangedEvent args)
+ {
+ // If our target has a TargetingComponent, that means they will take limb damage
+ // And if their attacker also has one, then we use that part.
+ if (_queryTargeting.TryComp(ent, out var targetEnt))
+ {
+ var damage = args.Damage;
+ TargetBodyPart? targetPart = null;
+
+ if (args.TargetPart != null)
+ {
+ targetPart = args.TargetPart;
+ }
+ else if (args.Origin.HasValue && _queryTargeting.TryComp(args.Origin.Value, out var targeter))
+ {
+ targetPart = targeter.Target;
+ // If the target is Torso then have a 33% chance to hit another part
+ if (targetPart.Value == TargetBodyPart.Torso)
+ {
+ var additionalPart = GetRandomPartSpread(_random, 10);
+ targetPart = targetPart.Value | additionalPart;
+ }
+ }
+ else
+ {
+ // If there's an origin in this case, that means it comes from an entity without TargetingComponent,
+ // such as an animal, so we attack a random part.
+ if (args.Origin.HasValue)
+ {
+ targetPart = GetRandomBodyPart(ent, targetEnt);
+ }
+ // Otherwise we damage all parts equally (barotrauma, explosions, etc).
+ else if (damage != null)
+ {
+ // Division by 2 cuz damaging all parts by the same damage by default is too much.
+ damage /= 2;
+ targetPart = TargetBodyPart.All;
+ }
+ }
+
+ if (targetPart == null)
+ return;
+
+ if (!TryChangePartDamage(ent, args.Damage, args.CanSever, args.CanEvade, args.PartMultiplier, targetPart.Value)
+ && args.CanEvade)
+ {
+ if (_net.IsServer)
+ _popup.PopupEntity(Loc.GetString("surgery-part-damage-evaded", ("user", Identity.Entity(ent, EntityManager))), ent);
+
+ args.Evaded = true;
+ }
+ }
+ }
+
+ private void OnBodyDamageModify(Entity bodyEnt, ref DamageModifyEvent args)
+ {
+ if (args.TargetPart != null)
+ {
+ var (targetType, _) = ConvertTargetBodyPart(args.TargetPart.Value);
+ args.Damage = args.Damage * GetPartDamageModifier(targetType);
+ }
+ }
+
+ private void OnPartDamageModify(Entity partEnt, ref DamageModifyEvent args)
+ {
+ if (partEnt.Comp.Body != null
+ && TryComp(partEnt.Comp.Body.Value, out DamageableComponent? damageable)
+ && damageable.DamageModifierSetId != null
+ && _prototypeManager.TryIndex(damageable.DamageModifierSetId, out var modifierSet))
+ // TODO: We need to add a check to see if the given armor covers this part to cancel or not.
+ args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifierSet);
+
+ if (_prototypeManager.TryIndex("PartDamage", out var partModifierSet))
+ args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, partModifierSet);
+
+ args.Damage = args.Damage * GetPartDamageModifier(partEnt.Comp.PartType);
+ }
+
+ private bool TryChangePartDamage(EntityUid entity,
+ DamageSpecifier damage,
+ bool canSever,
+ bool canEvade,
+ float partMultiplier,
+ TargetBodyPart targetParts)
+ {
+ var landed = false;
+ var targets = SharedTargetingSystem.GetValidParts();
+ foreach (var target in targets)
+ {
+ if (!targetParts.HasFlag(target))
+ continue;
+
+ var (targetType, targetSymmetry) = ConvertTargetBodyPart(target);
+ if (GetBodyChildrenOfType(entity, targetType, symmetry: targetSymmetry) is { } part)
+ {
+ if (canEvade && TryEvadeDamage(entity, GetEvadeChance(targetType)))
+ continue;
+
+ var damageResult = _damageable.TryChangeDamage(part.FirstOrDefault().Id, damage * partMultiplier, canSever: canSever);
+ if (damageResult != null && damageResult.GetTotal() != 0)
+ landed = true;
+ }
+ }
+
+ return landed;
+ }
+
+ private void OnDamageChanged(Entity partEnt, ref DamageChangedEvent args)
+ {
+ if (!TryComp(partEnt, out var damageable))
+ return;
+
+ var severed = false;
+ var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot;
+ var delta = args.DamageDelta;
+
+ if (args.CanSever
+ && partIdSlot is not null
+ && delta != null
+ && !HasComp(partEnt)
+ && !partEnt.Comp.Enabled
+ && damageable.TotalDamage >= partEnt.Comp.SeverIntegrity
+ && _severingDamageTypes.Any(damageType => delta.DamageDict.TryGetValue(damageType, out var value) && value > 0))
+ severed = true;
+
+ CheckBodyPart(partEnt, GetTargetBodyPart(partEnt), severed, damageable);
+
+ if (severed)
+ DropPart(partEnt);
+
+ Dirty(partEnt, partEnt.Comp);
+ }
+
+ ///
+ /// Gets the random body part rolling a number between 1 and 9, and returns
+ /// Torso if the result is 9 or more. The higher torsoWeight is, the higher chance to return it.
+ /// By default, the chance to return Torso is 50%.
+ ///
+ private static TargetBodyPart GetRandomPartSpread(IRobustRandom random, ushort torsoWeight = 9)
+ {
+ const int targetPartsAmount = 9;
+ // 5 = amount of target parts except Torso
+ return random.Next(1, targetPartsAmount + torsoWeight) switch
+ {
+ 1 => TargetBodyPart.Head,
+ 2 => TargetBodyPart.RightArm,
+ 3 => TargetBodyPart.RightHand,
+ 4 => TargetBodyPart.LeftArm,
+ 5 => TargetBodyPart.LeftHand,
+ 6 => TargetBodyPart.RightLeg,
+ 7 => TargetBodyPart.RightFoot,
+ 8 => TargetBodyPart.LeftLeg,
+ 9 => TargetBodyPart.LeftFoot,
+ _ => TargetBodyPart.Torso,
+ };
+ }
+
+ public TargetBodyPart? GetRandomBodyPart(EntityUid uid, TargetingComponent? target = null)
+ {
+ if (!Resolve(uid, ref target))
+ return null;
+
+ var totalWeight = target.TargetOdds.Values.Sum();
+ var randomValue = _random.NextFloat() * totalWeight;
+
+ foreach (var (part, weight) in target.TargetOdds)
+ {
+ if (randomValue <= weight)
+ return part;
+ randomValue -= weight;
+ }
+
+ return TargetBodyPart.Torso; // Default to torso if something goes wrong
+ }
+
+ ///
+ /// This should be called after body part damage was changed.
+ ///
+ protected void CheckBodyPart(
+ Entity partEnt,
+ TargetBodyPart? targetPart,
+ bool severed,
+ DamageableComponent? damageable = null)
+ {
+ if (!Resolve(partEnt, ref damageable))
+ return;
+
+ var integrity = damageable.TotalDamage;
+
+ // KILL the body part
+ if (partEnt.Comp.Enabled && integrity >= partEnt.Comp.IntegrityThresholds[TargetIntegrity.CriticallyWounded])
+ {
+ var ev = new BodyPartEnableChangedEvent(false);
+ RaiseLocalEvent(partEnt, ref ev);
+ }
+
+ // LIVE the body part
+ if (!partEnt.Comp.Enabled && integrity <= partEnt.Comp.IntegrityThresholds[partEnt.Comp.EnableIntegrity] && !severed)
+ {
+ var ev = new BodyPartEnableChangedEvent(true);
+ RaiseLocalEvent(partEnt, ref ev);
+ }
+
+ if (_queryTargeting.TryComp(partEnt.Comp.Body, out var targeting)
+ && HasComp(partEnt.Comp.Body))
+ {
+ var newIntegrity = GetIntegrityThreshold(partEnt.Comp, integrity.Float(), severed);
+ // We need to check if the part is dead to prevent the UI from showing dead parts as alive.
+ if (targetPart is not null &&
+ targeting.BodyStatus.ContainsKey(targetPart.Value) &&
+ targeting.BodyStatus[targetPart.Value] != TargetIntegrity.Dead)
+ {
+ targeting.BodyStatus[targetPart.Value] = newIntegrity;
+ if (targetPart.Value == TargetBodyPart.Torso)
+ targeting.BodyStatus[TargetBodyPart.Groin] = newIntegrity;
+
+ Dirty(partEnt.Comp.Body.Value, targeting);
+ }
+ // Revival events are handled by the server, so we end up being locked to a network event.
+ // I hope you like the _net.IsServer, Remuchi :)
+ if (_net.IsServer)
+ RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(partEnt.Comp.Body.Value)), partEnt.Comp.Body.Value);
+ }
+ }
+
+ ///
+ /// Gets the integrity of all body parts in the entity.
+ ///
+ public Dictionary GetBodyPartStatus(EntityUid entityUid)
+ {
+ var result = new Dictionary();
+
+ if (!TryComp(entityUid, out var body))
+ return result;
+
+ foreach (var part in SharedTargetingSystem.GetValidParts())
+ {
+ result[part] = TargetIntegrity.Severed;
+ }
+
+ foreach (var partComponent in GetBodyChildren(entityUid, body))
+ {
+ var targetBodyPart = GetTargetBodyPart(partComponent.Component.PartType, partComponent.Component.Symmetry);
+
+ if (targetBodyPart != null && TryComp(partComponent.Id, out var damageable))
+ result[targetBodyPart.Value] = GetIntegrityThreshold(partComponent.Component, damageable.TotalDamage.Float(), false);
+ }
+
+ // Hardcoded shitcode for Groin :)
+ result[TargetBodyPart.Groin] = result[TargetBodyPart.Torso];
+
+ 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.
+ ///
+ public TargetBodyPart? GetTargetBodyPart(BodyPartType type, BodyPartSymmetry symmetry)
+ {
+ return (type, symmetry) switch
+ {
+ (BodyPartType.Head, _) => TargetBodyPart.Head,
+ (BodyPartType.Torso, _) => TargetBodyPart.Torso,
+ (BodyPartType.Arm, BodyPartSymmetry.Left) => TargetBodyPart.LeftArm,
+ (BodyPartType.Arm, BodyPartSymmetry.Right) => TargetBodyPart.RightArm,
+ (BodyPartType.Hand, BodyPartSymmetry.Left) => TargetBodyPart.LeftHand,
+ (BodyPartType.Hand, BodyPartSymmetry.Right) => TargetBodyPart.RightHand,
+ (BodyPartType.Leg, BodyPartSymmetry.Left) => TargetBodyPart.LeftLeg,
+ (BodyPartType.Leg, BodyPartSymmetry.Right) => TargetBodyPart.RightLeg,
+ (BodyPartType.Foot, BodyPartSymmetry.Left) => TargetBodyPart.LeftFoot,
+ (BodyPartType.Foot, BodyPartSymmetry.Right) => TargetBodyPart.RightFoot,
+ _ => null
+ };
+ }
+
+ ///
+ /// Converts Enums from Targeting system to their BodyPartType equivalent.
+ ///
+ public (BodyPartType Type, BodyPartSymmetry Symmetry) ConvertTargetBodyPart(TargetBodyPart targetPart)
+ {
+ return targetPart switch
+ {
+ TargetBodyPart.Head => (BodyPartType.Head, BodyPartSymmetry.None),
+ TargetBodyPart.Torso => (BodyPartType.Torso, BodyPartSymmetry.None),
+ TargetBodyPart.Groin => (BodyPartType.Torso, BodyPartSymmetry.None), // TODO: Groin is not a part type yet
+ TargetBodyPart.LeftArm => (BodyPartType.Arm, BodyPartSymmetry.Left),
+ TargetBodyPart.LeftHand => (BodyPartType.Hand, BodyPartSymmetry.Left),
+ TargetBodyPart.RightArm => (BodyPartType.Arm, BodyPartSymmetry.Right),
+ TargetBodyPart.RightHand => (BodyPartType.Hand, BodyPartSymmetry.Right),
+ TargetBodyPart.LeftLeg => (BodyPartType.Leg, BodyPartSymmetry.Left),
+ TargetBodyPart.LeftFoot => (BodyPartType.Foot, BodyPartSymmetry.Left),
+ TargetBodyPart.RightLeg => (BodyPartType.Leg, BodyPartSymmetry.Right),
+ TargetBodyPart.RightFoot => (BodyPartType.Foot, BodyPartSymmetry.Right),
+ _ => (BodyPartType.Torso, BodyPartSymmetry.None)
+ };
+
+ }
+
+ public DamageSpecifier GetHealingSpecifier(BodyPartComponent part)
+ {
+ var damage = new DamageSpecifier()
+ {
+ DamageDict = new Dictionary()
+ {
+ { "Blunt", -part.SelfHealingAmount },
+ { "Slash", -part.SelfHealingAmount },
+ { "Piercing", -part.SelfHealingAmount },
+ { "Heat", -part.SelfHealingAmount },
+ { "Cold", -part.SelfHealingAmount },
+ { "Shock", -part.SelfHealingAmount },
+ { "Caustic", -part.SelfHealingAmount * 0.1}, // not much caustic healing
+ }
+ };
+
+ return damage;
+ }
+
+ ///
+ /// Fetches the damage multiplier for part integrity based on part types.
+ ///
+ /// TODO: Serialize this per body part.
+ public static float GetPartDamageModifier(BodyPartType partType)
+ {
+ return partType switch
+ {
+ BodyPartType.Head => 0.5f, // 50% damage, necks are hard to cut
+ BodyPartType.Torso => 1.0f, // 100% damage
+ BodyPartType.Arm => 0.7f, // 70% damage
+ BodyPartType.Hand => 0.7f, // 70% damage
+ BodyPartType.Leg => 0.7f, // 70% damage
+ BodyPartType.Foot => 0.7f, // 70% damage
+ _ => 0.5f
+ };
+ }
+
+ ///
+ /// Fetches the TargetIntegrity equivalent of the current integrity value for the body part.
+ ///
+ public static TargetIntegrity GetIntegrityThreshold(BodyPartComponent component, float integrity, bool severed)
+ {
+ if (severed)
+ return TargetIntegrity.Severed;
+ else if (!component.Enabled)
+ return TargetIntegrity.Disabled;
+
+ var targetIntegrity = TargetIntegrity.Healthy;
+ foreach (var threshold in component.IntegrityThresholds)
+ {
+ if (integrity <= threshold.Value)
+ targetIntegrity = threshold.Key;
+ }
+
+ return targetIntegrity;
+ }
+
+ ///
+ /// Fetches the chance to evade integrity damage for a body part.
+ /// Used when the entity is not dead, laying down, or incapacitated.
+ ///
+ public static float GetEvadeChance(BodyPartType partType)
+ {
+ return partType switch
+ {
+ BodyPartType.Head => 0.70f, // 70% chance to evade
+ BodyPartType.Arm => 0.20f, // 20% chance to evade
+ BodyPartType.Hand => 0.20f, // 20% chance to evade
+ BodyPartType.Leg => 0.20f, // 20% chance to evade
+ BodyPartType.Foot => 0.20f, // 20% chance to evade
+ BodyPartType.Torso => 0f, // 0% chance to evade
+ _ => 0f
+ };
+ }
+
+ public bool CanEvadeDamage(EntityUid uid)
+ {
+ if (!TryComp(uid, out var mobState)
+ || !TryComp(uid, out var standingState)
+ || _mobState.IsCritical(uid, mobState)
+ || _mobState.IsDead(uid, mobState)
+ || standingState.CurrentState == StandingState.Lying)
+ return false;
+
+ return true;
+ }
+
+ public bool TryEvadeDamage(EntityUid uid, float evadeChance)
+ {
+ if (!CanEvadeDamage(uid))
+ return false;
+
+ return _random.NextFloat() < evadeChance;
+ }
+
+}
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs
index a45966fcc37..013e302633b 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.cs
@@ -28,7 +28,7 @@ public abstract partial class SharedBodySystem : EntitySystem
///
public const string OrganSlotContainerIdPrefix = "body_organ_slot_";
- [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly IPrototypeManager Prototypes = default!;
[Dependency] protected readonly DamageableSystem Damageable = default!;
[Dependency] protected readonly MovementSpeedModifierSystem Movement = default!;
@@ -42,6 +42,9 @@ public override void Initialize()
InitializeBody();
InitializeParts();
+ // To try and mitigate the server load due to integrity checks, we set up a Job Queue.
+ InitializeIntegrityQueue();
+ InitializePartAppearances();
}
///
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 60dceea897e..603855679c4 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -2635,6 +2635,12 @@ public static readonly CVarDef
#endregion
+ #region Surgery
+
+ public static readonly CVarDef CanOperateOnSelf =
+ CVarDef.Create("surgery.can_operate_on_self", false, CVar.SERVERONLY);
+
+ #endregion
///
/// Set to true to disable parallel processing in the pow3r solver.
///
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs
index 4aaf380c47d..4d08735d810 100644
--- a/Content.Shared/Damage/Systems/DamageableSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageableSystem.cs
@@ -6,11 +6,14 @@
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Body.Systems;
using Content.Shared.Radiation.Events;
using Content.Shared.Rejuvenate;
+using Content.Shared.Targeting;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared.Damage
@@ -22,10 +25,12 @@ public sealed class DamageableSystem : EntitySystem
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
private EntityQuery _appearanceQuery;
private EntityQuery _damageableQuery;
private EntityQuery _mindContainerQuery;
-
+ private EntityQuery _targetingQuery;
public override void Initialize()
{
SubscribeLocalEvent(DamageableInit);
@@ -37,6 +42,7 @@ public override void Initialize()
_appearanceQuery = GetEntityQuery();
_damageableQuery = GetEntityQuery();
_mindContainerQuery = GetEntityQuery();
+ _targetingQuery = GetEntityQuery();
}
///
@@ -98,7 +104,8 @@ public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpeci
/// The damage changed event is used by other systems, such as damage thresholds.
///
public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null,
- bool interruptsDoAfters = true, EntityUid? origin = null)
+ bool interruptsDoAfters = true, EntityUid? origin = null, bool? canSever = null)
+
{
component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
component.TotalDamage = component.Damage.GetTotal();
@@ -109,7 +116,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList());
_appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance);
}
- RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin));
+ RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, canSever ?? true));
}
///
@@ -125,7 +132,8 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
/// null if the user had no applicable components that can take damage.
///
public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
- bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null)
+ bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null,
+ bool? canSever = true, bool? canEvade = false, float? partMultiplier = 1.00f, TargetBodyPart? targetPart = null)
{
if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false))
{
@@ -138,10 +146,10 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
return damage;
}
- var before = new BeforeDamageChangedEvent(damage, origin);
+ var before = new BeforeDamageChangedEvent(damage, origin, targetPart, canSever ?? true, canEvade ?? false, partMultiplier ?? 1.00f);
RaiseLocalEvent(uid.Value, ref before);
- if (before.Cancelled)
+ if (before.Cancelled || before.Evaded)
return null;
// Apply resistances
@@ -152,10 +160,10 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
{
// TODO DAMAGE PERFORMANCE
// use a local private field instead of creating a new dictionary here..
+ // TODO: We need to add a check to see if the given armor covers the targeted part (if any) to modify or not.
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
}
-
- var ev = new DamageModifyEvent(damage, origin);
+ var ev = new DamageModifyEvent(damage, origin, targetPart);
RaiseLocalEvent(uid.Value, ev);
damage = ev.Damage;
@@ -187,7 +195,7 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
}
if (delta.DamageDict.Count > 0)
- DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin);
+ DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin, canSever);
return delta;
}
@@ -257,6 +265,11 @@ private void OnRejuvenate(EntityUid uid, DamageableComponent component, Rejuvena
TryComp(uid, out var thresholds);
_mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage
SetAllDamage(uid, component, 0);
+ // Shitmed Start
+ if (HasComp(uid))
+ foreach (var part in _body.GetBodyChildren(uid))
+ RaiseLocalEvent(part.Id, new RejuvenateEvent());
+ // Shitmed End
_mobThreshold.SetAllowRevives(uid, false, thresholds);
}
@@ -286,7 +299,15 @@ private void DamageableHandleState(EntityUid uid, DamageableComponent component,
/// Raised before damage is done, so stuff can cancel it if necessary.
///
[ByRefEvent]
- public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false);
+ public record struct BeforeDamageChangedEvent(
+ DamageSpecifier Damage,
+ EntityUid? Origin = null,
+ TargetBodyPart? TargetPart = null,
+ bool CanSever = true,
+ bool CanEvade = false,
+ float PartMultiplier = 1.00f,
+ bool Evaded = false,
+ bool Cancelled = false);
///
/// Raised on an entity when damage is about to be dealt,
@@ -299,16 +320,17 @@ public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent
{
// Whenever locational damage is a thing, this should just check only that bit of armour.
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
-
public readonly DamageSpecifier OriginalDamage;
public DamageSpecifier Damage;
public EntityUid? Origin;
+ public readonly TargetBodyPart? TargetPart;
- public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null)
+ public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null, TargetBodyPart? targetPart = null)
{
OriginalDamage = damage;
Damage = damage;
Origin = origin;
+ TargetPart = targetPart;
}
}
@@ -347,11 +369,17 @@ public sealed class DamageChangedEvent : EntityEventArgs
///
public readonly EntityUid? Origin;
- public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin)
+ ///
+ /// Can this damage event sever parts?
+ ///
+ public readonly bool CanSever;
+
+ public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin, bool canSever = true)
{
Damageable = damageable;
DamageDelta = damageDelta;
Origin = origin;
+ CanSever = canSever;
if (DamageDelta == null)
return;
diff --git a/Content.Shared/Hands/Components/HandsComponent.cs b/Content.Shared/Hands/Components/HandsComponent.cs
index 919d55f294a..a7464e5bac7 100644
--- a/Content.Shared/Hands/Components/HandsComponent.cs
+++ b/Content.Shared/Hands/Components/HandsComponent.cs
@@ -29,6 +29,7 @@ public sealed partial class HandsComponent : Component
///
/// List of hand-names. These are keys for . The order of this list determines the order in which hands are iterated over.
///
+ [ViewVariables]
public List SortedHands = new();
///
diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
index 40f2c5bbd59..ead824e712e 100644
--- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
+++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
@@ -4,6 +4,7 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
+using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Containers;
@@ -17,6 +18,7 @@ public abstract partial class SharedHandsSystem
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!;
diff --git a/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs
new file mode 100644
index 00000000000..afe78a15172
--- /dev/null
+++ b/Content.Shared/Humanoid/Events/ProfileLoadFinishedEvent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Humanoid.Events;
+
+///
+/// Raised on an entity when their profile has finished being loaded
+///
+public sealed class ProfileLoadFinishedEvent : EntityEventArgs { }
+
diff --git a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
index 0f8b940bd66..17912e400a5 100644
--- a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
+++ b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
@@ -47,18 +47,30 @@ public static IEnumerable Sublayers(HumanoidVisualLayers l
yield return HumanoidVisualLayers.LArm;
yield return HumanoidVisualLayers.LHand;
break;
+ case HumanoidVisualLayers.LHand:
+ yield return HumanoidVisualLayers.LHand;
+ break;
case HumanoidVisualLayers.RArm:
yield return HumanoidVisualLayers.RArm;
yield return HumanoidVisualLayers.RHand;
break;
+ case HumanoidVisualLayers.RHand:
+ yield return HumanoidVisualLayers.RHand;
+ break;
case HumanoidVisualLayers.LLeg:
yield return HumanoidVisualLayers.LLeg;
yield return HumanoidVisualLayers.LFoot;
break;
+ case HumanoidVisualLayers.LFoot:
+ yield return HumanoidVisualLayers.LFoot;
+ break;
case HumanoidVisualLayers.RLeg:
yield return HumanoidVisualLayers.RLeg;
yield return HumanoidVisualLayers.RFoot;
break;
+ case HumanoidVisualLayers.RFoot:
+ yield return HumanoidVisualLayers.RFoot;
+ break;
case HumanoidVisualLayers.Chest:
yield return HumanoidVisualLayers.Chest;
yield return HumanoidVisualLayers.Tail;
diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
index a1e8bec2cd8..f33c65b5915 100644
--- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
+++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
@@ -5,6 +5,7 @@
using Content.Shared.Examine;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Humanoid.Events;
using Content.Shared.IdentityManagement;
using Content.Shared.Preferences;
using Content.Shared.HeightAdjust;
@@ -440,6 +441,7 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile,
humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned
Dirty(humanoid);
+ RaiseLocalEvent(uid, new ProfileLoadFinishedEvent());
}
///
diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs
index 3f88ca4e20a..699acc87ad3 100644
--- a/Content.Shared/Input/ContentKeyFunctions.cs
+++ b/Content.Shared/Input/ContentKeyFunctions.cs
@@ -61,6 +61,12 @@ public static class ContentKeyFunctions
public static readonly BoundKeyFunction ToggleStanding = "ToggleStanding";
public static readonly BoundKeyFunction ToggleCrawlingUnder = "ToggleCrawlingUnder";
public static readonly BoundKeyFunction LookUp = "LookUp";
+ public static readonly BoundKeyFunction TargetHead = "TargetHead";
+ public static readonly BoundKeyFunction TargetTorso = "TargetTorso";
+ public static readonly BoundKeyFunction TargetLeftArm = "TargetLeftArm";
+ public static readonly BoundKeyFunction TargetRightArm = "TargetRightArm";
+ public static readonly BoundKeyFunction TargetLeftLeg = "TargetLeftLeg";
+ public static readonly BoundKeyFunction TargetRightLeg = "TargetRightLeg";
public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp";
public static readonly BoundKeyFunction ArcadeDown = "ArcadeDown";
diff --git a/Content.Shared/Inventory/InventoryComponent.cs b/Content.Shared/Inventory/InventoryComponent.cs
index 02b3a5b2583..edc8e6641c1 100644
--- a/Content.Shared/Inventory/InventoryComponent.cs
+++ b/Content.Shared/Inventory/InventoryComponent.cs
@@ -16,6 +16,7 @@ public sealed partial class InventoryComponent : Component
[DataField] public Dictionary Displacements = [];
public SlotDefinition[] Slots = Array.Empty();
+
public ContainerSlot[] Containers = Array.Empty();
[DataDefinition]
diff --git a/Content.Shared/Inventory/InventorySystem.Slots.cs b/Content.Shared/Inventory/InventorySystem.Slots.cs
index e0f2a695576..97f63262dbc 100644
--- a/Content.Shared/Inventory/InventorySystem.Slots.cs
+++ b/Content.Shared/Inventory/InventorySystem.Slots.cs
@@ -1,17 +1,21 @@
+using Content.Shared.Random;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Inventory.Events;
using Content.Shared.Storage;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
namespace Content.Shared.Inventory;
-
public partial class InventorySystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!;
-
+ [Dependency] private readonly RandomHelperSystem _randomHelper = default!;
+ [Dependency] private readonly ISerializationManager _serializationManager = default!;
private void InitializeSlots()
{
SubscribeLocalEvent(OnInit);
@@ -57,7 +61,8 @@ protected virtual void OnInit(EntityUid uid, InventoryComponent component, Compo
if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate))
return;
- component.Slots = invTemplate.Slots;
+ _serializationManager.CopyTo(invTemplate.Slots, ref component.Slots, notNullableOverride: true);
+
component.Containers = new ContainerSlot[component.Slots.Length];
for (var i = 0; i < component.Containers.Length; i++)
{
@@ -115,7 +120,7 @@ public bool TryGetSlot(EntityUid uid, string slot, [NotNullWhen(true)] out SlotD
foreach (var slotDef in inventory.Slots)
{
- if (!slotDef.Name.Equals(slot))
+ if (!slotDef.Name.Equals(slot) || slotDef.Disabled)
continue;
slotDefinition = slotDef;
return true;
@@ -170,6 +175,37 @@ private IEnumerable ListViewVariablesSlots(EntityUid uid, InventoryCompo
}
}
+ public void SetSlotStatus(EntityUid uid, string slotName, bool isDisabled, InventoryComponent? inventory = null)
+ {
+ if (!Resolve(uid, ref inventory))
+ return;
+
+ foreach (var slot in inventory.Slots)
+ {
+ if (slot.Name != slotName)
+ continue;
+
+ if (isDisabled)
+ {
+ if (!TryGetSlotContainer(uid, slotName, out var container, out _, inventory))
+ break;
+
+ if (container.ContainedEntity is { } entityUid && TryComp(entityUid, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted)
+ {
+ _transform.AttachToGridOrMap(entityUid, transform);
+ _randomHelper.RandomOffset(entityUid, 0.5f);
+ }
+ //_containerSystem.ShutdownContainer(container);
+ }
+ //else
+ //_containerSystem.EnsureContainer(uid, slotName);
+ slot.Disabled = isDisabled;
+ break;
+ }
+
+ Dirty(uid, inventory);
+ }
+
///
/// Enumerator for iterating over an inventory's slot containers. Also has methods that skip empty containers.
/// It should be safe to add or remove items while enumerating.
@@ -182,12 +218,12 @@ public struct InventorySlotEnumerator
private int _nextIdx = 0;
public static InventorySlotEnumerator Empty = new(Array.Empty(), Array.Empty());
- public InventorySlotEnumerator(InventoryComponent inventory, SlotFlags flags = SlotFlags.All)
+ public InventorySlotEnumerator(InventoryComponent inventory, SlotFlags flags = SlotFlags.All)
: this(inventory.Slots, inventory.Containers, flags)
{
}
- public InventorySlotEnumerator(SlotDefinition[] slots, ContainerSlot[] containers, SlotFlags flags = SlotFlags.All)
+ public InventorySlotEnumerator(SlotDefinition[] slots, ContainerSlot[] containers, SlotFlags flags = SlotFlags.All)
{
DebugTools.Assert(flags != SlotFlags.NONE);
DebugTools.AssertEqual(slots.Length, containers.Length);
@@ -203,7 +239,7 @@ public bool MoveNext([NotNullWhen(true)] out ContainerSlot? container)
var i = _nextIdx++;
var slot = _slots[i];
- if ((slot.SlotFlags & _flags) == 0)
+ if ((slot.SlotFlags & _flags) == 0 || slot.Disabled)
continue;
container = _containers[i];
@@ -221,7 +257,7 @@ public bool NextItem(out EntityUid item)
var i = _nextIdx++;
var slot = _slots[i];
- if ((slot.SlotFlags & _flags) == 0)
+ if ((slot.SlotFlags & _flags) == 0 || slot.Disabled)
continue;
var container = _containers[i];
diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs
index a4d77767e37..0d900688fcc 100644
--- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs
+++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs
@@ -55,4 +55,9 @@ public sealed partial class SlotDefinition
/// Entity blacklist for CanEquip checks.
///
[DataField("blacklist")] public EntityWhitelist? Blacklist = null;
+
+ ///
+ /// Is this slot disabled? Could be due to severing or other reasons.
+ ///
+ [DataField] public bool Disabled;
}
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs
new file mode 100644
index 00000000000..bab7e405ad5
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryCloseIncisionConditionComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryCloseIncisionConditionComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryLarvaConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryLarvaConditionComponent.cs
new file mode 100644
index 00000000000..3aac5951c6f
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryLarvaConditionComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryLarvaConditionComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryMarkingConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryMarkingConditionComponent.cs
new file mode 100644
index 00000000000..f22b1f682e2
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryMarkingConditionComponent.cs
@@ -0,0 +1,27 @@
+using Content.Shared.Body.Organ;
+using Content.Shared.Humanoid;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryMarkingConditionComponent : Component
+{
+
+ [DataField]
+ public bool Inverse;
+
+ ///
+ /// The marking category to check for.
+ ///
+ [DataField]
+ public HumanoidVisualLayers MarkingCategory = default!;
+
+ ///
+ /// Can be either a segment of a marking ID, or an entire ID that will be checked
+ /// against the entity to validate that the marking is not already present.
+ ///
+ [DataField]
+ public String MatchString = "";
+}
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs
new file mode 100644
index 00000000000..0c43549e669
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryOperatingTableConditionComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryOperatingTableConditionComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganConditionComponent.cs
new file mode 100644
index 00000000000..c8c475f115a
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganConditionComponent.cs
@@ -0,0 +1,18 @@
+using Content.Shared.Body.Organ;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryOrganConditionComponent : Component
+{
+ [DataField]
+ public ComponentRegistry? Organ;
+
+ [DataField]
+ public bool Inverse;
+
+ [DataField]
+ public bool Reattaching;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryPartConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartConditionComponent.cs
new file mode 100644
index 00000000000..08a89eb9e1b
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartConditionComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Body.Part;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryPartConditionComponent : Component
+{
+ [DataField]
+ public BodyPartType Part;
+
+ [DataField]
+ public BodyPartSymmetry? Symmetry;
+
+ [DataField]
+ public bool Inverse;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryPartPresentCondition.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartPresentCondition.cs
new file mode 100644
index 00000000000..608f90ba4cb
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartPresentCondition.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryPartPresentConditionComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs
new file mode 100644
index 00000000000..fb51ab5b060
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartRemovedConditionComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Body.Part;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryPartRemovedConditionComponent : Component
+{
+ [DataField]
+ public BodyPartType Part;
+
+ [DataField]
+ public BodyPartSymmetry? Symmetry;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryValidEvent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryValidEvent.cs
new file mode 100644
index 00000000000..da769a457ac
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryValidEvent.cs
@@ -0,0 +1,9 @@
+using Content.Shared.Body.Part;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+///
+/// Raised on the entity that is receiving surgery.
+///
+[ByRefEvent]
+public record struct SurgeryValidEvent(EntityUid Body, EntityUid Part, bool Cancelled = false, BodyPartType PartType = default, BodyPartSymmetry? Symmetry = default);
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryWoundedConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryWoundedConditionComponent.cs
new file mode 100644
index 00000000000..2279fcd0440
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryWoundedConditionComponent.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Body.Part;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Conditions;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryWoundedConditionComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryCompletedEvent.cs b/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryCompletedEvent.cs
new file mode 100644
index 00000000000..a0e040fbe7a
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryCompletedEvent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Medical.Surgery.Effects.Complete;
+
+///
+/// Raised on the entity that received the surgery.
+///
+[ByRefEvent]
+public record struct SurgeryCompletedEvent;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs b/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs
new file mode 100644
index 00000000000..2077dfa53b8
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Complete/SurgeryRemoveLarvaComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Effects.Complete;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryRemoveLarvaComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs
new file mode 100644
index 00000000000..0db43011a08
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryDamageChangeEffectComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryDamageChangeEffectComponent : Component
+{
+ [DataField]
+ public DamageSpecifier Damage = default!;
+
+ [DataField]
+ public float SleepModifier = 0.5f;
+
+ [DataField]
+ public bool IsConsumable;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs
new file mode 100644
index 00000000000..e375865277f
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgerySpecialDamageChangeEffectComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgerySpecialDamageChangeEffectComponent : Component
+{
+ [DataField]
+ public string DamageType = "";
+
+ [DataField]
+ public bool IsConsumable;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs
new file mode 100644
index 00000000000..61300425a7d
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepCavityEffectComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryStepCavityEffectComponent : Component
+{
+ [DataField]
+ public string Action = "Insert";
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs
new file mode 100644
index 00000000000..02e8b749eed
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepEmoteEffectComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SurgeryStepEmoteEffectComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public ProtoId Emote = "Scream";
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs
new file mode 100644
index 00000000000..766713c6f68
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryStepSpawnEffect.cs
@@ -0,0 +1,13 @@
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using System.ComponentModel.DataAnnotations;
+
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SurgeryStepSpawnEffectComponent : Component
+{
+ [DataField(required: true), AutoNetworkedField]
+ public EntProtoId Entity;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs
new file mode 100644
index 00000000000..58db1422d8f
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Effects/Step/SurgeryTendWoundsEffectComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+namespace Content.Shared.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SurgeryTendWoundsEffectComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public string MainGroup = "Brute";
+
+ [DataField, AutoNetworkedField]
+ public bool IsAutoRepeatable = true;
+
+ [DataField, AutoNetworkedField]
+ public DamageSpecifier Damage = default!;
+
+ [DataField, AutoNetworkedField]
+ public float HealMultiplier = 0.07f;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/OperatingTableComponent.cs b/Content.Shared/Medical/Surgery/OperatingTableComponent.cs
new file mode 100644
index 00000000000..fa0ccf72580
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/OperatingTableComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class OperatingTableComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs
new file mode 100644
index 00000000000..0686bf53095
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs
@@ -0,0 +1,755 @@
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Organ;
+using Content.Shared.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.Medical.Surgery.Conditions;
+using Content.Shared.Medical.Surgery.Effects.Step;
+using Content.Shared.Medical.Surgery.Steps;
+using Content.Shared.Medical.Surgery.Steps.Parts;
+using Content.Shared.Medical.Surgery.Tools;
+using Content.Shared.Mood;
+using Content.Shared.Inventory;
+using Content.Shared.Item;
+using Content.Shared.Popups;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed.TypeParsers;
+using System.Linq;
+
+namespace Content.Shared.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);
+ Subs.BuiEvents(SurgeryUIKey.Key, subs =>
+ {
+ subs.Event(OnSurgeryTargetStepChosen);
+ });
+ }
+
+ private void SubSurgery(EntityEventRefHandler onStep,
+ EntityEventRefHandler onComplete) where TComp : IComponent
+ {
+ SubscribeLocalEvent(onStep);
+ SubscribeLocalEvent(onComplete);
+ }
+
+ private void OnToolStep(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (ent.Comp.Tool != null)
+ {
+ foreach (var reg in ent.Comp.Tool.Values)
+ {
+ if (!AnyHaveComp(args.Tools, reg.Component, out var tool))
+ return;
+
+ if (_net.IsServer &&
+ TryComp(tool, out SurgeryToolComponent? toolComp) &&
+ toolComp.EndSound != null)
+ {
+ _audio.PlayEntity(toolComp.EndSound, args.User, tool);
+ }
+ }
+ }
+
+ if (ent.Comp.Add != null)
+ {
+ foreach (var reg in ent.Comp.Add.Values)
+ {
+ var compType = reg.Component.GetType();
+ if (HasComp(args.Part, compType))
+ continue;
+ AddComp(args.Part, _compFactory.GetComponent(compType));
+ }
+ }
+
+ if (ent.Comp.Remove != null)
+ {
+ foreach (var reg in ent.Comp.Remove.Values)
+ {
+ RemComp(args.Part, reg.Component.GetType());
+ }
+ }
+
+ if (ent.Comp.BodyRemove != null)
+ {
+ foreach (var reg in ent.Comp.BodyRemove.Values)
+ {
+ RemComp(args.Body, reg.Component.GetType());
+ }
+ }
+
+ if (!HasComp(args.Body))
+ RaiseLocalEvent(args.Body, new MoodEffectEvent("SurgeryPain"));
+
+ if (!_inventory.TryGetSlotEntity(args.User, "gloves", out var _)
+ || !_inventory.TryGetSlotEntity(args.User, "mask", out var _))
+ {
+ 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 void OnToolCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args)
+ {
+ // Lord this function is fucking bloated now. Need to clean it up so its less spammy.
+ if (ent.Comp.Add != null)
+ {
+ foreach (var reg in ent.Comp.Add.Values)
+ {
+ if (!HasComp(args.Part, reg.Component.GetType()))
+ {
+ args.Cancelled = true;
+ return;
+ }
+ }
+ }
+
+ if (ent.Comp.Remove != null)
+ {
+ foreach (var reg in ent.Comp.Remove.Values)
+ {
+ if (HasComp(args.Part, reg.Component.GetType()))
+ {
+ args.Cancelled = true;
+ return;
+ }
+ }
+ }
+
+ if (ent.Comp.BodyRemove != null)
+ {
+ foreach (var reg in ent.Comp.BodyRemove.Values)
+ {
+ if (HasComp(args.Body, reg.Component.GetType()))
+ {
+ args.Cancelled = true;
+ return;
+ }
+ }
+ }
+ }
+
+ 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))
+ {
+ 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 HashSet();
+
+ foreach (var reg in ent.Comp.Tool.Values)
+ {
+ if (!AnyHaveComp(args.Tools, reg.Component, out var withComp))
+ {
+ args.Invalid = StepInvalidReason.MissingTool;
+
+ if (reg.Component is ISurgeryToolComponent tool)
+ args.Popup = $"You need {tool.ToolName} to perform this step!";
+
+ return;
+ }
+
+ args.ValidTools.Add(withComp);
+ }
+ }
+ }
+
+ private EntProtoId? GetProtoId(EntityUid entityUid)
+ {
+ if (!TryComp(entityUid, out var metaData))
+ return null;
+
+ return metaData.EntityPrototype?.ID;
+ }
+
+ // 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 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);
+ var bonusPerType = bonus / group.Length;
+
+ foreach (var type in group)
+ {
+ adjustedDamage.DamageDict[type] -= bonusPerType;
+ }
+
+ 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);
+ _body.ChangeSlotState((tool, partComp), false);
+ EnsureComp(tool);
+ var ev = new BodyPartAttachedEvent((tool, partComp));
+ RaiseLocalEvent(args.Body, ref ev);
+ }
+ }
+ }
+
+ 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);
+ 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 not { Valid: true } body ||
+ GetEntity(args.Part) is not { Valid: true } targetPart ||
+ !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, user, out var surgery, out var part, out var step))
+ {
+ return;
+ }
+
+ if (!PreviousStepsComplete(body, part, surgery, args.Step) ||
+ IsStepComplete(body, part, args.Step, surgery))
+ return;
+
+ 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(args.Surgery, args.Step);
+ // 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)
+ && surgerySpeedMod is not null)
+ duration = duration / surgerySpeedMod.SpeedModifier;
+
+ var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part)
+ {
+ BreakOnUserMove = true,
+ BreakOnTargetMove = true,
+ CancelDuplicate = true,
+ DuplicateCondition = DuplicateConditions.SameEvent,
+ NeedHand = true,
+ BreakOnHandChange = true,
+ };
+
+ _doAfter.TryStartDoAfter(doAfter);
+ }
+
+ 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 HashSet? 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)
+ {
+ foreach (var tool in tools)
+ {
+ if (HasComp(tool, component.GetType()))
+ {
+ withComp = tool;
+ return true;
+ }
+ }
+
+ withComp = default;
+ return false;
+ }
+}
diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs
new file mode 100644
index 00000000000..d7b049b0245
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs
@@ -0,0 +1,283 @@
+using System.Linq;
+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;
+using Content.Shared.Damage;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Body.Components;
+using Content.Shared.Buckle.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.GameTicking;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Standing;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Medical.Surgery;
+
+public abstract partial class SharedSurgerySystem : EntitySystem
+{
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IComponentFactory _compFactory = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly RotateToFaceSystem _rotateToFace = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ private readonly Dictionary _surgeries = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRoundRestartCleanup);
+
+ SubscribeLocalEvent(OnTargetDoAfter);
+ SubscribeLocalEvent(OnCloseIncisionValid);
+ //SubscribeLocalEvent(OnLarvaValid);
+ SubscribeLocalEvent(OnPartConditionValid);
+ SubscribeLocalEvent(OnOrganConditionValid);
+ SubscribeLocalEvent(OnWoundedValid);
+ SubscribeLocalEvent(OnPartRemovedConditionValid);
+ SubscribeLocalEvent(OnPartPresentConditionValid);
+ SubscribeLocalEvent(OnMarkingPresentValid);
+ //SubscribeLocalEvent(OnRemoveLarva);
+
+ InitializeSteps();
+ }
+
+ private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
+ {
+ _surgeries.Clear();
+ }
+
+ private void OnTargetDoAfter(Entity ent, ref SurgeryDoAfterEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ if (args.Cancelled
+ || args.Handled
+ || args.Target is not { } target
+ || !IsSurgeryValid(ent, target, args.Surgery, args.Step, args.User, out var surgery, out var part, out var step)
+ || !PreviousStepsComplete(ent, part, surgery, args.Step)
+ || !CanPerformStep(args.User, ent, part, step, false))
+ {
+ Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery.");
+ return;
+ }
+
+ args.Repeat = (HasComp(step) && !IsStepComplete(ent, part, args.Step, surgery));
+ var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User), surgery);
+ RaiseLocalEvent(step, ref ev);
+ RefreshUI(ent);
+ }
+
+ private void OnCloseIncisionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (!HasComp(args.Part) ||
+ !HasComp(args.Part) ||
+ !HasComp(args.Part) ||
+ !HasComp(args.Part) ||
+ !HasComp(args.Part))
+ {
+ args.Cancelled = true;
+ }
+ }
+
+ private void OnWoundedValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (!TryComp(args.Body, out DamageableComponent? damageable)
+ || !TryComp(args.Part, out DamageableComponent? partDamageable)
+ || damageable.TotalDamage <= 0
+ && partDamageable.TotalDamage <= 0
+ && !HasComp(args.Part))
+ args.Cancelled = true;
+ }
+
+ /*private void OnLarvaValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (!TryComp(args.Body, out VictimInfectedComponent? infected))
+ args.Cancelled = true;
+
+ // The larva has fully developed and surgery is now impossible
+ if (infected != null && infected.SpawnedLarva != null)
+ args.Cancelled = true;
+ }*/
+ private void OnPartConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (!TryComp(args.Part, out var part))
+ {
+ args.Cancelled = true;
+ return;
+ }
+
+ var typeMatch = part.PartType == ent.Comp.Part;
+ var symmetryMatch = ent.Comp.Symmetry == null || part.Symmetry == ent.Comp.Symmetry;
+ var valid = typeMatch && symmetryMatch;
+
+ if (ent.Comp.Inverse ? valid : !valid)
+ args.Cancelled = true;
+ }
+
+ private void OnOrganConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (!TryComp(args.Part, out var partComp)
+ || partComp.Body != args.Body
+ || ent.Comp.Organ == null)
+ {
+ args.Cancelled = true;
+ return;
+ }
+
+ foreach (var reg in ent.Comp.Organ.Values)
+ {
+ if (_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs)
+ && organs.Count > 0)
+ {
+ if (ent.Comp.Inverse
+ && (!ent.Comp.Reattaching
+ || ent.Comp.Reattaching
+ && !organs.Any(organ => HasComp(organ.Id))))
+ args.Cancelled = true;
+ }
+ else if (!ent.Comp.Inverse)
+ args.Cancelled = true;
+ }
+ }
+
+ private void OnPartRemovedConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ var results = _body.GetBodyChildrenOfType(args.Body, ent.Comp.Part, symmetry: ent.Comp.Symmetry);
+ if (results is not { } || !results.Any())
+ return;
+
+ if (!results.Any(part => HasComp(part.Id)))
+ args.Cancelled = true;
+ }
+
+ private void OnPartPresentConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (args.Part == EntityUid.Invalid
+ || !HasComp(args.Part))
+ args.Cancelled = true;
+ }
+
+ private void OnMarkingPresentValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ var markingCategory = MarkingCategoriesConversion.FromHumanoidVisualLayers(ent.Comp.MarkingCategory);
+
+ var hasMarking = TryComp(args.Body, out HumanoidAppearanceComponent? bodyAppearance)
+ && bodyAppearance.MarkingSet.Markings.TryGetValue(markingCategory, out var markingList)
+ && markingList.Any(marking => marking.MarkingId.Contains(ent.Comp.MatchString));
+
+ if ((!ent.Comp.Inverse && hasMarking) || (ent.Comp.Inverse && !hasMarking))
+ args.Cancelled = true;
+ }
+
+ /*private void OnRemoveLarva(Entity ent, ref SurgeryCompletedEvent args)
+ {
+ RemCompDeferred(ent);
+ }*/
+
+ protected bool IsSurgeryValid(EntityUid body, EntityUid targetPart, EntProtoId surgery, EntProtoId stepId,
+ EntityUid user, out Entity surgeryEnt, out EntityUid part, out EntityUid step)
+ {
+ surgeryEnt = default;
+ part = default;
+ step = default;
+
+ if (!HasComp(body) ||
+ !IsLyingDown(body, user) ||
+ GetSingleton(surgery) is not { } surgeryEntId ||
+ !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) ||
+ !surgeryComp.Steps.Contains(stepId) ||
+ GetSingleton(stepId) is not { } stepEnt
+ || !HasComp(targetPart)
+ && !HasComp(targetPart))
+ return false;
+
+
+ var ev = new SurgeryValidEvent(body, targetPart);
+ if (_timing.IsFirstTimePredicted)
+ {
+ RaiseLocalEvent(stepEnt, ref ev);
+ RaiseLocalEvent(surgeryEntId, ref ev);
+ }
+
+ if (ev.Cancelled)
+ return false;
+
+ surgeryEnt = (surgeryEntId, surgeryComp);
+ part = targetPart;
+ step = stepEnt;
+ return true;
+ }
+
+ public EntityUid? GetSingleton(EntProtoId surgeryOrStep)
+ {
+ if (!_prototypes.HasIndex(surgeryOrStep))
+ return null;
+
+ // This (for now) assumes that surgery entity data remains unchanged between client
+ // and server
+ // if it does not you get the bullet
+ if (!_surgeries.TryGetValue(surgeryOrStep, out var ent) || TerminatingOrDeleted(ent))
+ {
+ ent = Spawn(surgeryOrStep, MapCoordinates.Nullspace);
+ _surgeries[surgeryOrStep] = ent;
+ }
+
+ return ent;
+ }
+
+ private List GetTools(EntityUid surgeon)
+ {
+ return _hands.EnumerateHeld(surgeon).ToList();
+ }
+
+ public bool IsLyingDown(EntityUid entity, EntityUid user)
+ {
+ if (_standing.IsDown(entity))
+ return true;
+
+ if (TryComp(entity, out BuckleComponent? buckle) &&
+ TryComp(buckle.BuckledTo, out StrapComponent? strap))
+ {
+ var rotation = strap.Rotation;
+ if (rotation.GetCardinalDir() is Direction.West or Direction.East)
+ return true;
+ }
+
+ _popup.PopupEntity(Loc.GetString("surgery-error-laying"), user, user);
+
+ return false;
+ }
+
+ protected virtual void RefreshUI(EntityUid body)
+ {
+ }
+}
diff --git a/Content.Shared/Medical/Surgery/StepInvalidReason.cs b/Content.Shared/Medical/Surgery/StepInvalidReason.cs
new file mode 100644
index 00000000000..dbea495d088
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/StepInvalidReason.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared.Medical.Surgery;
+
+public enum StepInvalidReason
+{
+ None,
+ MissingSkills,
+ NeedsOperatingTable,
+ Armor,
+ MissingTool,
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/BleedersClampedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/BleedersClampedComponent.cs
new file mode 100644
index 00000000000..24d4fd99354
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/BleedersClampedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BleedersClampedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartReattachedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartReattachedComponent.cs
new file mode 100644
index 00000000000..30739c821b4
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartReattachedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BodyPartReattachedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartSawedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartSawedComponent.cs
new file mode 100644
index 00000000000..0838175d9a1
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/BodyPartSawedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BodyPartSawedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/IncisionOpenComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/IncisionOpenComponent.cs
new file mode 100644
index 00000000000..f41319549cd
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/IncisionOpenComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class IncisionOpenComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs
new file mode 100644
index 00000000000..7e597e88ef9
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/InternalBleedersClampedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class InternalBleedersClampedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/OrganReattachedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/OrganReattachedComponent.cs
new file mode 100644
index 00000000000..9e034598e68
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/OrganReattachedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class OrganReattachedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/PartRemovedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/PartRemovedComponent.cs
new file mode 100644
index 00000000000..ced1d1b9848
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/PartRemovedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PartsRemovedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/RibcageOpenComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/RibcageOpenComponent.cs
new file mode 100644
index 00000000000..d8942bd9665
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/RibcageOpenComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class RibcageOpenComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/RibcageSawedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/RibcageSawedComponent.cs
new file mode 100644
index 00000000000..527b3dc99aa
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/RibcageSawedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class RibcageSawedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/Parts/SkinRetractedComponent.cs b/Content.Shared/Medical/Surgery/Steps/Parts/SkinRetractedComponent.cs
new file mode 100644
index 00000000000..6f75a83f17f
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/Parts/SkinRetractedComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SkinRetractedComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryAddMarkingStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryAddMarkingStepComponent.cs
new file mode 100644
index 00000000000..b945c8d909e
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryAddMarkingStepComponent.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Humanoid;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryAddMarkingStepComponent : Component
+{
+ ///
+ /// The marking category to add the marking to.
+ ///
+ [DataField]
+ public HumanoidVisualLayers MarkingCategory = default!;
+
+ ///
+ /// Can be either a segment of a marking ID, or an entire ID that will be checked
+ /// against the entity to validate that the marking is not already present.
+ ///
+ [DataField]
+ public String MatchString = "";
+
+ ///
+ /// What type of organ is required for this surgery?
+ ///
+ [DataField]
+ public ComponentRegistry? Organ;
+
+ ///
+ /// Component name for accent that will be applied.
+ ///
+ [DataField]
+ public ComponentRegistry? Accent;
+}
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryAddOrganStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryAddOrganStepComponent.cs
new file mode 100644
index 00000000000..2d169879f9c
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryAddOrganStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryAddOrganStepComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryAddPartStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryAddPartStepComponent.cs
new file mode 100644
index 00000000000..0229552ae8a
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryAddPartStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryAddPartStepComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryAffixOrganStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryAffixOrganStepComponent.cs
new file mode 100644
index 00000000000..5f82cbe4251
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryAffixOrganStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryAffixOrganStepComponent : Component;
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryAffixPartStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryAffixPartStepComponent.cs
new file mode 100644
index 00000000000..cc080e8be0b
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryAffixPartStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryAffixPartStepComponent : Component;
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryCanPerformStepEvent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryCanPerformStepEvent.cs
new file mode 100644
index 00000000000..cd6d0fd4556
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryCanPerformStepEvent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Inventory;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[ByRefEvent]
+public record struct SurgeryCanPerformStepEvent(
+ EntityUid User,
+ EntityUid Body,
+ List Tools,
+ SlotFlags TargetSlots,
+ string? Popup = null,
+ StepInvalidReason Invalid = StepInvalidReason.None,
+ HashSet? ValidTools = null
+) : IInventoryRelayEvent;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs
new file mode 100644
index 00000000000..349815379b7
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryCutLarvaRootsStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryCutLarvaRootsStepComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveMarkingStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveMarkingStepComponent.cs
new file mode 100644
index 00000000000..47368a154c0
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveMarkingStepComponent.cs
@@ -0,0 +1,29 @@
+using Robust.Shared.Prototypes;
+using Content.Shared.Humanoid;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryRemoveMarkingStepComponent : Component
+{
+ ///
+ /// The category the marking belongs to.
+ ///
+ [DataField]
+ public HumanoidVisualLayers MarkingCategory = default!;
+
+ ///
+ /// Can be either a segment of a marking ID, or an entire ID that will be checked
+ /// against the entity to validate that the marking is present.
+ ///
+ [DataField]
+ public String MatchString = "";
+
+ ///
+ /// Will this step spawn an item as a result of removing the markings? If so, which?
+ ///
+ [DataField]
+ public EntProtoId? ItemSpawn = default!;
+
+}
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs
new file mode 100644
index 00000000000..66f2ea62fd3
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryRemoveOrganStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryRemoveOrganStepComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryRemovePartStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryRemovePartStepComponent.cs
new file mode 100644
index 00000000000..f55f3d1b7bf
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryRemovePartStepComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryRemovePartStepComponent : Component;
\ No newline at end of file
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/Content.Shared/Medical/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs
new file mode 100644
index 00000000000..ed28aab1db7
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryStepCompleteCheckEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[ByRefEvent]
+public record struct SurgeryStepCompleteCheckEvent(EntityUid Body, EntityUid Part, EntityUid Surgery, bool Cancelled = false);
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs
new file mode 100644
index 00000000000..1c6e801a42c
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent]
+[Prototype("SurgerySteps")]
+public sealed partial class SurgeryStepComponent : Component
+{
+
+ [DataField]
+ public ComponentRegistry? Tool;
+
+ [DataField]
+ public ComponentRegistry? Add;
+
+ [DataField]
+ public ComponentRegistry? Remove;
+
+ [DataField]
+ public ComponentRegistry? BodyRemove;
+}
diff --git a/Content.Shared/Medical/Surgery/SurgeryComponent.cs b/Content.Shared/Medical/Surgery/SurgeryComponent.cs
new file mode 100644
index 00000000000..3d3c8952344
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryComponent.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Prototype("Surgeries")]
+public sealed partial class SurgeryComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public int Priority;
+
+ [DataField, AutoNetworkedField]
+ public EntProtoId? Requirement;
+
+ [DataField(required: true), AutoNetworkedField]
+ public List Steps = new();
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SurgeryDoAfterEvent.cs b/Content.Shared/Medical/Surgery/SurgeryDoAfterEvent.cs
new file mode 100644
index 00000000000..e61cfbd8e47
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryDoAfterEvent.cs
@@ -0,0 +1,18 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Medical.Surgery;
+
+[Serializable, NetSerializable]
+public sealed partial class SurgeryDoAfterEvent : SimpleDoAfterEvent
+{
+ public readonly EntProtoId Surgery;
+ public readonly EntProtoId Step;
+
+ public SurgeryDoAfterEvent(EntProtoId surgery, EntProtoId step)
+ {
+ Surgery = surgery;
+ Step = step;
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SurgerySpeedModifierComponent.cs b/Content.Shared/Medical/Surgery/SurgerySpeedModifierComponent.cs
new file mode 100644
index 00000000000..b9b586b8f58
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgerySpeedModifierComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgerySpeedModifierComponent : Component
+{
+ [DataField]
+ public float SpeedModifier = 1.5f;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SurgeryStepDamageEvent.cs b/Content.Shared/Medical/Surgery/SurgeryStepDamageEvent.cs
new file mode 100644
index 00000000000..781cf81acf6
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryStepDamageEvent.cs
@@ -0,0 +1,9 @@
+using Content.Shared.Damage;
+
+namespace Content.Shared.Medical.Surgery;
+
+///
+/// Raised on the target entity.
+///
+[ByRefEvent]
+public record struct SurgeryStepDamageEvent(EntityUid User, EntityUid Body, EntityUid Part, EntityUid Surgery, DamageSpecifier Damage, float PartMultiplier);
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SurgeryStepEvent.cs b/Content.Shared/Medical/Surgery/SurgeryStepEvent.cs
new file mode 100644
index 00000000000..9123c6d0d50
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryStepEvent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Medical.Surgery;
+
+///
+/// Raised on the step entity.
+///
+[ByRefEvent]
+public record struct SurgeryStepEvent(EntityUid User, EntityUid Body, EntityUid Part, List Tools, EntityUid Surgery);
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs
new file mode 100644
index 00000000000..d2d7f8d4620
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryTargetComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgeryTargetComponent : Component
+{
+ [DataField]
+ public bool CanOperate = true;
+}
diff --git a/Content.Shared/Medical/Surgery/SurgeryUI.cs b/Content.Shared/Medical/Surgery/SurgeryUI.cs
new file mode 100644
index 00000000000..2572aaca65a
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryUI.cs
@@ -0,0 +1,32 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Medical.Surgery;
+
+[Serializable, NetSerializable]
+public enum SurgeryUIKey
+{
+ Key
+}
+
+[Serializable, NetSerializable]
+public sealed class SurgeryBuiState(Dictionary> choices) : BoundUserInterfaceState
+{
+ public readonly Dictionary> Choices = choices;
+}
+
+[Serializable, NetSerializable]
+public sealed class SurgeryBuiRefreshMessage : BoundUserInterfaceMessage
+{
+}
+
+[Serializable, NetSerializable]
+public sealed class SurgeryStepChosenBuiMsg(NetEntity part, EntProtoId surgery, EntProtoId step, bool isBody) : BoundUserInterfaceMessage
+{
+ public readonly NetEntity Part = part;
+ public readonly EntProtoId Surgery = surgery;
+ public readonly EntProtoId Step = step;
+
+ // Used as a marker for whether or not we're hijacking surgery by applying it on the body itself.
+ public readonly bool IsBody = isBody;
+}
diff --git a/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs b/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs
new file mode 100644
index 00000000000..9d41401d7f8
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/SurgeryUiRefreshEvent.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Medical.Surgery;
+
+[Serializable, NetSerializable]
+public sealed class SurgeryUiRefreshEvent : EntityEventArgs
+{
+ public NetEntity Uid { get; }
+
+ public SurgeryUiRefreshEvent(NetEntity uid)
+ {
+ Uid = uid;
+ }
+}
diff --git a/Content.Shared/Medical/Surgery/Tools/BoneGelComponent.cs b/Content.Shared/Medical/Surgery/Tools/BoneGelComponent.cs
new file mode 100644
index 00000000000..50f614afe16
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/BoneGelComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BoneGelComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "bone gel";
+
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/BoneSawComponent.cs b/Content.Shared/Medical/Surgery/Tools/BoneSawComponent.cs
new file mode 100644
index 00000000000..2b7c76eeac9
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/BoneSawComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BoneSawComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a bone saw";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/BoneSetterComponent.cs b/Content.Shared/Medical/Surgery/Tools/BoneSetterComponent.cs
new file mode 100644
index 00000000000..e68d64b4075
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/BoneSetterComponent.cs
@@ -0,0 +1,6 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class BoneSetterComponent : Component;
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/CauteryComponent.cs b/Content.Shared/Medical/Surgery/Tools/CauteryComponent.cs
new file mode 100644
index 00000000000..9a56f809733
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/CauteryComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class CauteryComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a cautery";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/HemostatComponent.cs b/Content.Shared/Medical/Surgery/Tools/HemostatComponent.cs
new file mode 100644
index 00000000000..3e869e1c094
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/HemostatComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class HemostatComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a hemostat";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/ISurgeryToolComponent.cs b/Content.Shared/Medical/Surgery/Tools/ISurgeryToolComponent.cs
new file mode 100644
index 00000000000..7fb4491f0c5
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/ISurgeryToolComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Shared.Medical.Surgery.Tools;
+
+public interface ISurgeryToolComponent
+{
+ [DataField]
+ public string ToolName { get; }
+
+ // Mostly intended for discardable or non-reusable tools.
+ [DataField]
+ public bool? Used { get; set; }
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/RetractorComponent.cs b/Content.Shared/Medical/Surgery/Tools/RetractorComponent.cs
new file mode 100644
index 00000000000..bf57c70729a
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/RetractorComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class RetractorComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a retractor";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/ScalpelComponent.cs b/Content.Shared/Medical/Surgery/Tools/ScalpelComponent.cs
new file mode 100644
index 00000000000..40fb4af556d
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/ScalpelComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ScalpelComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a scalpel";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/SurgeryToolComponent.cs b/Content.Shared/Medical/Surgery/Tools/SurgeryToolComponent.cs
new file mode 100644
index 00000000000..f0dd96ecb0d
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/SurgeryToolComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SurgeryToolComponent : Component
+{
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? StartSound;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? EndSound;
+}
\ No newline at end of file
diff --git a/Content.Shared/Medical/Surgery/Tools/SurgicalDrillComponent.cs b/Content.Shared/Medical/Surgery/Tools/SurgicalDrillComponent.cs
new file mode 100644
index 00000000000..c8995edcec8
--- /dev/null
+++ b/Content.Shared/Medical/Surgery/Tools/SurgicalDrillComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Medical.Surgery.Tools;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SurgicalDrillComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a surgical drill";
+ public bool? Used { get; set; } = null;
+}
\ No newline at end of file
diff --git a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
index 08af1a36a7b..14e7c2a3fcf 100644
--- a/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
+++ b/Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
@@ -1,3 +1,5 @@
+using Content.Shared.Targeting;
+using Content.Shared.Body.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.MedicalScanner;
@@ -14,8 +16,10 @@ public sealed class HealthAnalyzerScannedUserMessage : BoundUserInterfaceMessage
public bool? ScanMode;
public bool? Bleeding;
public bool? Unrevivable;
+ public Dictionary? Body; // Shitmed
+ public NetEntity? Part; // Shitmed
- public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable)
+ public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperature, float bloodLevel, bool? scanMode, bool? bleeding, bool? unrevivable, Dictionary? body, NetEntity? part = null)
{
TargetEntity = targetEntity;
Temperature = temperature;
@@ -23,6 +27,16 @@ public HealthAnalyzerScannedUserMessage(NetEntity? targetEntity, float temperatu
ScanMode = scanMode;
Bleeding = bleeding;
Unrevivable = unrevivable;
+ Body = body; // Shitmed
+ Part = part; // Shitmed
}
}
+[Serializable, NetSerializable]
+public sealed class HealthAnalyzerPartMessage(NetEntity? owner, TargetBodyPart? bodyPart) : BoundUserInterfaceMessage
+{
+ public readonly NetEntity? Owner = owner;
+ public readonly TargetBodyPart? BodyPart = bodyPart;
+
+}
+
diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs b/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs
index 2fa522dea59..b65d970eb96 100644
--- a/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs
+++ b/Content.Shared/Mobs/Systems/MobStateSystem.StateMachine.cs
@@ -1,5 +1,6 @@
using Content.Shared.Database;
using Content.Shared.Mobs.Components;
+using Content.Shared.Body.Organ;
namespace Content.Shared.Mobs.Systems;
@@ -102,6 +103,9 @@ private void ChangeState(EntityUid target, MobStateComponent component, MobState
if (oldState == newState || !component.AllowedStates.Contains(newState))
return;
+ if (oldState == MobState.Dead && HasComp(target))
+ return;
+
OnExitState(target, component, oldState);
component.CurrentState = newState;
OnEnterState(target, component, newState);
diff --git a/Content.Shared/Standing/SharedLayingDownSystem.cs b/Content.Shared/Standing/SharedLayingDownSystem.cs
index 9fa4717045c..b2bb5add5f4 100644
--- a/Content.Shared/Standing/SharedLayingDownSystem.cs
+++ b/Content.Shared/Standing/SharedLayingDownSystem.cs
@@ -5,6 +5,8 @@
using Content.Shared.Input;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems;
+using Content.Shared.Body.Components;
+using Content.Shared.Standing;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Robust.Shared.Configuration;
@@ -141,7 +143,9 @@ public bool TryStandUp(EntityUid uid, LayingDownComponent? layingDown = null, St
|| !Resolve(uid, ref layingDown, false)
|| standingState.CurrentState is not StandingState.Lying
|| !_mobState.IsAlive(uid)
- || TerminatingOrDeleted(uid))
+ || TerminatingOrDeleted(uid)
+ || !TryComp(uid, out var body)
+ || body.LegEntities.Count == 0)
return false;
var args = new DoAfterArgs(EntityManager, uid, layingDown.StandingUpTime, new StandingUpDoAfterEvent(), uid)
diff --git a/Content.Shared/Targeting/Events.cs b/Content.Shared/Targeting/Events.cs
new file mode 100644
index 00000000000..1b090be3e86
--- /dev/null
+++ b/Content.Shared/Targeting/Events.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Targeting;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Targeting.Events;
+
+[Serializable, NetSerializable]
+public sealed class TargetChangeEvent : EntityEventArgs
+{
+ public NetEntity Uid { get; }
+ public TargetBodyPart BodyPart { get; }
+ public TargetChangeEvent(NetEntity uid, TargetBodyPart bodyPart)
+ {
+ Uid = uid;
+ BodyPart = bodyPart;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class TargetIntegrityChangeEvent : EntityEventArgs
+{
+ public NetEntity Uid { get; }
+ public bool RefreshUi { get; }
+ public TargetIntegrityChangeEvent(NetEntity uid, bool refreshUi = true)
+ {
+ Uid = uid;
+ RefreshUi = refreshUi;
+ }
+}
+
+public sealed class RefreshInventorySlotsEvent : EntityEventArgs
+{
+ public string SlotName { get; }
+
+ public RefreshInventorySlotsEvent(string slotName)
+ {
+ SlotName = slotName;
+ }
+}
diff --git a/Content.Shared/Targeting/SharedTargetingSystem.cs b/Content.Shared/Targeting/SharedTargetingSystem.cs
new file mode 100644
index 00000000000..4f2248683e6
--- /dev/null
+++ b/Content.Shared/Targeting/SharedTargetingSystem.cs
@@ -0,0 +1,26 @@
+namespace Content.Shared.Targeting;
+public abstract class SharedTargetingSystem : EntitySystem
+{
+ ///
+ /// Returns all Valid target body parts as an array.
+ ///
+ public static TargetBodyPart[] GetValidParts()
+ {
+ var parts = new[]
+ {
+ TargetBodyPart.Head,
+ TargetBodyPart.Torso,
+ //TargetBodyPart.Groin,
+ TargetBodyPart.LeftArm,
+ TargetBodyPart.LeftHand,
+ TargetBodyPart.LeftLeg,
+ TargetBodyPart.LeftFoot,
+ TargetBodyPart.RightArm,
+ TargetBodyPart.RightHand,
+ TargetBodyPart.RightLeg,
+ TargetBodyPart.RightFoot,
+ };
+
+ return parts;
+ }
+}
diff --git a/Content.Shared/Targeting/TargetBodyPart.cs b/Content.Shared/Targeting/TargetBodyPart.cs
new file mode 100644
index 00000000000..dd894545445
--- /dev/null
+++ b/Content.Shared/Targeting/TargetBodyPart.cs
@@ -0,0 +1,31 @@
+namespace Content.Shared.Targeting;
+
+
+///
+/// Represents and enum of possible target parts.
+///
+///
+/// To get all body parts as an Array, use static
+/// method in SharedTargetingSystem GetValidParts.
+///
+[Flags]
+public enum TargetBodyPart : ushort
+{
+ Head = 1,
+ Torso = 1 << 1,
+ Groin = 1 << 2,
+ LeftArm = 1 << 3,
+ LeftHand = 1 << 4,
+ RightArm = 1 << 5,
+ RightHand = 1 << 6,
+ LeftLeg = 1 << 7,
+ LeftFoot = 1 << 8,
+ RightLeg = 1 << 9,
+ RightFoot = 1 << 10,
+
+ Hands = LeftHand | RightHand,
+ Arms = LeftArm | RightArm,
+ Legs = LeftLeg | RightLeg,
+ Feet = LeftFoot | RightFoot,
+ All = Head | Torso | Groin | LeftArm | LeftHand | RightArm | RightHand | LeftLeg | LeftFoot | RightLeg | RightFoot,
+}
diff --git a/Content.Shared/Targeting/TargetIntegrity.cs b/Content.Shared/Targeting/TargetIntegrity.cs
new file mode 100644
index 00000000000..9b4515fcfae
--- /dev/null
+++ b/Content.Shared/Targeting/TargetIntegrity.cs
@@ -0,0 +1,13 @@
+namespace Content.Shared.Targeting;
+public enum TargetIntegrity
+{
+ Healthy = 0,
+ LightlyWounded = 1,
+ SomewhatWounded = 2,
+ ModeratelyWounded = 3,
+ HeavilyWounded = 4,
+ CriticallyWounded = 5,
+ Severed = 6,
+ Dead = 7,
+ Disabled = 8,
+}
\ No newline at end of file
diff --git a/Content.Shared/Targeting/TargetingComponent.cs b/Content.Shared/Targeting/TargetingComponent.cs
new file mode 100644
index 00000000000..cb74beee32f
--- /dev/null
+++ b/Content.Shared/Targeting/TargetingComponent.cs
@@ -0,0 +1,59 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Targeting;
+
+///
+/// Controls entity limb targeting for actions.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TargetingComponent : Component
+{
+ [ViewVariables, AutoNetworkedField]
+ public TargetBodyPart Target = TargetBodyPart.Torso;
+
+ ///
+ /// What odds does the entity have of targeting each body part?
+ ///
+ [DataField]
+ public Dictionary TargetOdds = new()
+ {
+ { TargetBodyPart.Head, 0.1f },
+ { TargetBodyPart.Torso, 0.3f },
+ { TargetBodyPart.Groin, 0.1f },
+ { TargetBodyPart.LeftArm, 0.1f },
+ { TargetBodyPart.LeftHand, 0.05f },
+ { TargetBodyPart.RightArm, 0.1f },
+ { TargetBodyPart.RightHand, 0.05f },
+ { TargetBodyPart.LeftLeg, 0.1f },
+ { TargetBodyPart.LeftFoot, 0.05f },
+ { TargetBodyPart.RightLeg, 0.1f },
+ { TargetBodyPart.RightFoot, 0.05f }
+ };
+
+ ///
+ /// What is the current integrity of each body part?
+ ///
+ [ViewVariables, AutoNetworkedField]
+ public Dictionary BodyStatus = new()
+ {
+ { TargetBodyPart.Head, TargetIntegrity.Healthy },
+ { TargetBodyPart.Torso, TargetIntegrity.Healthy },
+ { TargetBodyPart.Groin, TargetIntegrity.Healthy },
+ { TargetBodyPart.LeftArm, TargetIntegrity.Healthy },
+ { TargetBodyPart.LeftHand, TargetIntegrity.Healthy },
+ { TargetBodyPart.RightArm, TargetIntegrity.Healthy },
+ { TargetBodyPart.RightHand, TargetIntegrity.Healthy },
+ { TargetBodyPart.LeftLeg, TargetIntegrity.Healthy },
+ { TargetBodyPart.LeftFoot, TargetIntegrity.Healthy },
+ { TargetBodyPart.RightLeg, TargetIntegrity.Healthy },
+ { TargetBodyPart.RightFoot, TargetIntegrity.Healthy }
+ };
+
+ ///
+ /// What noise does the entity play when swapping targets?
+ ///
+ [DataField]
+ public string SwapSound = "/Audio/Effects/toggleoncombat.ogg";
+}
diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
index 5fc92ce37a9..2999b7aed7c 100644
--- a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
+++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
@@ -87,6 +87,12 @@ public sealed partial class MeleeWeaponComponent : Component
[DataField, AutoNetworkedField]
public FixedPoint2 ClickDamageModifier = FixedPoint2.New(1);
+ ///
+ /// Part damage is multiplied by this amount for single-target attacks
+ ///
+ [DataField, AutoNetworkedField]
+ public float ClickPartDamageMultiplier = 1.00f;
+
// TODO: Temporarily 1.5 until interactionoutline is adjusted to use melee, then probably drop to 1.2
///
/// Nearest edge range to hit an entity.
@@ -106,6 +112,12 @@ public sealed partial class MeleeWeaponComponent : Component
[DataField, AutoNetworkedField]
public float HeavyDamageBaseModifier = 1.2f;
+ ///
+ /// Part damage is multiplied by this amount for heavy swings
+ ///
+ [DataField, AutoNetworkedField]
+ public float HeavyPartDamageMultiplier = 0.5f;
+
///
/// Total width of the angle for wide attacks.
///
diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
index 45115a72cc7..72047666f8c 100644
--- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
+++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
@@ -501,7 +501,7 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity
RaiseLocalEvent(target.Value, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
- var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user);
+ var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin: user, partMultiplier: component.ClickPartDamageMultiplier);
if (damageResult != null && damageResult.Any())
{
@@ -640,7 +640,7 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU
RaiseLocalEvent(entity, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
- var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user);
+ var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin: user, partMultiplier: component.HeavyPartDamageMultiplier);
if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
{
diff --git a/Resources/Audio/Medical/Surgery/attributions.yml b/Resources/Audio/Medical/Surgery/attributions.yml
new file mode 100644
index 00000000000..c88a3e0b70f
--- /dev/null
+++ b/Resources/Audio/Medical/Surgery/attributions.yml
@@ -0,0 +1,49 @@
+- files: ["cautery1.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery1.ogg"
+
+- files: ["cautery2.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery2.ogg"
+
+- files: ["hemostat.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/hemostat.ogg"
+
+- files: ["organ1.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ1.ogg"
+
+- files: ["organ2.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ2.ogg"
+
+- files: ["retractor1.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor1.ogg"
+
+- files: ["retractor2.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor2.ogg"
+
+- files: ["saw.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/saw.ogg"
+
+- files: ["scalpel1.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel1.ogg"
+
+- files: ["scalpel2.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel2.ogg"
\ No newline at end of file
diff --git a/Resources/Audio/Medical/Surgery/cautery1.ogg b/Resources/Audio/Medical/Surgery/cautery1.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..fbd9f2b4d86257e87ec704b2138249f9c60efe65
GIT binary patch
literal 34770
zcmagG1ymhDvo5-Em*8%}EqH(gm*50F1{&P7v{;MGF2ONVDfjQgQ>zg^eHGv72|EG%!*xyPs
znC#og|GwTvzERStnzT`)|NOtMA&7sB*Z{h!nXM_Kti1_|rJ26+U-l&8BrHrUOiUk{
z*hr}5jSQU(%&bjFM68|7>}{;AjI14Lpn-AkfRBibkgTwXJddapiGrh(p_vUV&?qD;
zqbMMx$n(}oQCv|_@gcS3f
zvO#4Bw3dg%IM;sqe+sz2>mUK0}uARl0vpf52P;x!l}
zH3VZ*{5rV=++cE>I6A=s)+p
z=f>UJ{degkV4&>xT#4H>xny{`zqoQC0)ooHL56QCA%Tk}qlwKEFEloLB6n-0=~_yJuG*bxlak&DQa57^Vg{MGOv
z2(T%?Jbj-%+ZPA60f*1P{l!suC|+vpS_AvrrFsgtQ!V_e9o
zyV)feX)`}U71O9%N|Mw5TF`PFcN?%J&2UOzcFJU`!?-wNYQevn8(JDnMaeYKhP$7R
z+>fgV^fQ}Ig(+E51;(mPrtTHDU;wqy_%y197R7)3{sW5=?ey^hXS9x5j
z0!P2UD<=Vg0#N^=_`iz3Q2vMF{Mhfb!_;-7EF<)9MfuNh?&Ip8s6vR$0E+3^0Tf3x
z>}5Jsa*0_KG|a2h6~xO+&=mahD8Q&BhS37hxPP4_DA|6PrU>wg|7o}j@`-nZQ~%?h
zxFnE6qX6zH76t`Y9wkL36=y58B&U_8K&?fmjRnt*g$Nz&p#L3M|Lr*-z-a>h`eeL*
zIP*ZdzlM=bGn%cgaB`{RK%tHz*#FZ2AW(NSqU7H=A}7x%k>lSYEzyXQ&`GBg5qN&2~tm?ruZ%=ED-h}L2@9F5Az^%FOL1V_)xa+I5S*E
z{2&)qLEN-B4MQRhD-A^;3gcX`gaO0M&>8-yGzA0-<1_;V0iy!D>Hd=f)F2Q^
zHyH2{q-HY|0ijZXHf_l#;@BiHC?wHECNPW^(4`j=*v8n{00t~CO#s}K^uju&nI3o*8Qh}M4
zR-~1d9oJgZ{VYEglvDE5N-)~^sHKK&z40+pV<959^r)3~q!SRtyxMO=(8ffdwZZ`q
zGHNRjJPii#;3h0q`t6>$w{M;Dsu1A@q5y1wyL4T0M-cy2!kjdImbRZnF;aXmJq}I6lqP;!9ww*YtAs^P{JgwHP2sHm
zDM>z-gc(Epw7Pjo|EzlWI|Edf{d~i5akzr|Hv+(FKRX{;P@*6oLsEi4c@zM0-n4*N
zNj{o_cu51msvsXrQi8b=L&KVRo&Xq;kES6`(~qHG#n7MnzMlkOHS{L@uZ-Q~cz{)k
zf2@Q=MMA9nXnNcmEB{Cdahk?h2`OM!5)w6m^6Hk%^Xc-I?7*x5RIi>lRneeqVP4S>2&X8(k(8)uz_PGr-oukHt9TP$(ubuH2?$rPU@x3Ee
zNL0+v%0~bM1FMd?RQL1$TDiVCL;t*f#hY;2nuJ(D_};R5I9L3%KENtgJ{%aKVcAlc
zXbuok<-;2GmK`kFZ)erZ-(*MvZwQB|mK$Xt>i9r__E%ZVN3P+6M-Y
zqy`5P2E4eR1c*D(xCJ==lQ{Vp2B>5OIDlOWFbn|Q;y54+(&8G73sBPpe3hW32}G3;
zrziljW_SW1pR=5dS0w-@^e>L+BI+YD2az}`ve3Q@kdmM(@dY$m;RHaTpl?*&yrO=Q
zAGRWJo<))ZfP!&FZ?ZZa2y_BW=;Uu4p#Vk0fcSCp0J&TysQRN^2Lx1FvW(-Xj5JYp39{`vqM@WK;ngT!x4GjPvz@y{UB~l#Zt3E)vboIOY$=+d{SU(TFAEf4r7
zjzKx#K^HQK3QGW}dcz250APlHZ2{5$-G<7&{|{OKp!Waui~lP47h146KpPDq
z05a;oH42~x_b-9_3&sF6AeD;;IR9&lM+0R1P4D96-w+9?=8fzLvikCa
zVc-k`f&w6EK-tyZlgyfskUMBB@QCmO0S?n<5FpfmLByC4RYWxOQr}#CZjh>`?jVd0
zVnjrJ7{UQdH