Reverts "Add prosthetic limb" surgery to involve targeting limbs, rather than targeting chest. (Adds stumps) (#95252)

## About The Pull Request

- The `prosthetic replacement` surgical operation has been reverted to
be closer to how it used to work: The operation is done targeting the
limb that's missing

The change was made out of necessity, as surgical state was tied to
limbs - you had to operate on the chest to re-attach limbs because there
was no limb to operate on.

To circumvent that, I have done the unthinkable of adding stumps when
you are dismembered.

- Missing limbs are now represented as an invisible, un-removable,
un-interactable limb.

Making this change was not as difficult as originally anticipated, and
(at least surface level) seems to have broken very little.

Surprisingly little had to change to make this work. 

Direct accesses to `mob.bodyparts` was changed to `mob.get_bodyparts()`
with an optional `include_stumps` argument.
Similarly, `get_bodypart()` had an optional `include_stumps` added. 

This means we ultimately barely needed to change anything, and in fact,
some loops/checks were able to be streamlined.

## Why It's Good For The Game

- As mentioned, this change was out of necessity and was easily the
least intuitive part of the broader changes. Reverting it back to how it
used to work should make it far easier for people to pick up on, and
means we can cut out a bunch of bespoke instruction sets that I had to
include.

- The addition of stumps also adds a ton of future potential - code wise
it allows for stuff like better damage tracking (we can transfer damage
between limb <-> stump rather than limb <-> chest), and feature we can
do "fun" stuff like have stumps bleed on dismemberment that you can
bandage.

## Changelog

🆑 Melbert
del: "Add prosthetic limb" surgical operation has been reverted to be a
bit closer to how it used to work - you operate on the missing limb /
limb stump, rather than on the chest.
refactor: Missing limbs are now represented as limb stumps. In practice
this should change nothing (for now), as no features were rewritten to
make use of these besides surgery. Please report any oddities with
missing limbs, however.
/🆑
This commit is contained in:
MrMelbert
2026-03-20 14:32:41 +13:00
committed by GitHub
parent d0c6d60fc3
commit da64374423
128 changed files with 566 additions and 386 deletions
+4 -1
View File
@@ -1,8 +1,11 @@
/datum/unit_test/limbsanity
/datum/unit_test/limbsanity/Run()
for(var/path in subtypesof(/obj/item/bodypart) - list(/obj/item/bodypart/arm, /obj/item/bodypart/leg)) /// removes the abstract items.
for(var/path in valid_subtypesof(/obj/item/bodypart)) /// removes the abstract items.
var/obj/item/bodypart/part = path
if(part::bodypart_flags & BODYPART_STUMP)
continue // stumps don't need to have icons
if(part::is_dimorphic)
if(!icon_exists(UNLINT(part::should_draw_greyscale ? part::icon_greyscale : part::icon_static), "[part::limb_id]_[part::body_zone]_m"))
TEST_FAIL("[path] does not have a valid icon for male variants")
@@ -20,7 +20,7 @@
TEST_ASSERT_NULL(organ.bodypart_owner, "Organ '[organ.name] kept reference to bodypart after forceMove into nullspace.")
// 3. replace all bodyparts with new ones and place the previously removed organs into the new bodyparts
for(var/obj/item/bodypart/bodypart as anything in hollow_boy.bodyparts)
for(var/obj/item/bodypart/bodypart as anything in hollow_boy.get_bodyparts())
var/obj/item/bodypart/replacement = allocate(bodypart.type)
for(var/obj/item/organ/organ as anything in removed_organs)
if(replacement.body_zone != deprecise_zone(organ.zone))
@@ -35,3 +35,22 @@
TEST_ASSERT(organ in hollow_boy.organs, "Organ '[organ.name] was put in an empty bodypart that replaced a humans, but the organ did not come with.")
TEST_ASSERT(organ.owner == hollow_boy, "Organ '[organ.name]'s owner was not properly updated to the new human after being placed in a replacement bodypart.")
TEST_ASSERT(organ.bodypart_owner in hollow_boy.bodyparts, "Organ '[organ.name]'s bodypart_owner was not properly updated to the new bodypart after being placed in a replacement bodypart.")
/// Tests that gibbing results in a head with a brain
/datum/unit_test/gibbing_organ_transfer
/datum/unit_test/gibbing_organ_transfer/Run()
var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent)
var/list/bodyparts_for_cleanup = dummy.bodyparts.Copy()
var/list/organs_for_cleanup = dummy.organs.Copy()
var/obj/item/organ/brain/original_brain = dummy.get_organ_slot(ORGAN_SLOT_BRAIN)
var/obj/item/bodypart/head/original_head = dummy.get_bodypart(BODY_ZONE_HEAD)
dummy.gib(ALL)
TEST_ASSERT(QDELETED(dummy), "Dummy was not deleted after gibbing.")
TEST_ASSERT(!QDELETED(original_head), "Original head was deleted after gibbing.")
TEST_ASSERT(!QDELETED(original_brain), "Original brain was deleted after gibbing.")
TEST_ASSERT(original_brain.loc == original_head, "Original brain was not transferred to the head after gibbing.")
QDEL_LIST(organs_for_cleanup)
QDEL_LIST(bodyparts_for_cleanup)
+20
View File
@@ -14,3 +14,23 @@
TEST_ASSERT(!HAS_TRAIT_FROM(dummy, TRAIT_AGEUSIA, NO_TONGUE_TRAIT), "Dummy has ageusia on init, when it should've been removed by its default tongue.")
TEST_ASSERT(!dummy.is_blind_from(NO_EYES), "Dummy is blind on init, when it should've been removed by its default eyes.")
TEST_ASSERT(!HAS_TRAIT_FROM(dummy, TRAIT_DEAF, NO_EARS), "Dummy is deaf on init, when it should've been removed by its default ears.")
/// Tests that we can change a mob's hand count without everything breaking
/datum/unit_test/many_armed_humans
/datum/unit_test/many_armed_humans/Run()
var/mob/living/carbon/human/consistent/dummy = allocate(/mob/living/carbon/human/consistent)
dummy.change_number_of_hands(4)
/// Tests spawned humans have the correct bodypart order
/datum/unit_test/human_bodypart_order
/datum/unit_test/human_bodypart_order/Run()
var/mob/living/carbon/human/consistent/dummy = allocate(/mob/living/carbon/human/consistent)
var/list/obj/item/bodypart/bodyparts = dummy.get_bodyparts()
TEST_ASSERT(bodyparts[1].body_zone == BODY_ZONE_CHEST, "First bodypart in bodyparts list is not the chest, this is important for human rendering")
TEST_ASSERT(bodyparts[2].body_zone == BODY_ZONE_HEAD, "Second bodypart in bodyparts list is not the head, this is important for human rendering")
var/list/obj/item/bodypart/bodyparts_by_zone = dummy.get_bodyparts_by_zones()
TEST_ASSERT(bodyparts_by_zone[1] == BODY_ZONE_CHEST, "First bodypart in bodyparts_by_zone list is not the chest, this is important for human rendering")
TEST_ASSERT(bodyparts_by_zone[2] == BODY_ZONE_HEAD, "Second bodypart in bodyparts_by_zone list is not the head, this is important for human rendering")
+2 -2
View File
@@ -57,9 +57,9 @@
TEST_ASSERT_EQUAL(bobs_head.real_name, "Bob", "Bob's head does not remember that it is from Bob")
// Put Bob's head onto Alice's body
var/datum/surgery_operation/prosthetic_replacement/surgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__]
var/datum/surgery_operation/limb/prosthetic_replacement/surgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__]
user.put_in_active_hand(bobs_head)
UNLINT(surgery.success(alice.get_bodypart(BODY_ZONE_CHEST), user, bobs_head, list()))
UNLINT(surgery.success(alice.get_bodypart(BODY_ZONE_HEAD, TRUE), user, bobs_head, list()))
TEST_ASSERT(!isnull(alice.get_bodypart(BODY_ZONE_HEAD)), "Alice has no head after prosthetic replacement")
TEST_ASSERT_EQUAL(alice.get_visible_name(), "Bob", "Bob's head was transplanted onto Alice's body, but their name is not Bob")