Files
MrMelbert da64374423 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.
/🆑
2026-03-20 14:32:41 +13:00

313 lines
12 KiB
Plaintext

/**
* Can this atom be curshed by the vending machine
* Arguments
*
* * atom/atom_target - the atom we are checking for
*/
/proc/check_atom_crushable(atom/atom_target)
/// Contains structures and items that vendors shouldn't crush when we land on them.
var/static/list/vendor_uncrushable_objects = list(
/obj/structure/chair,
/obj/machinery/conveyor,
) + GLOB.WALLITEMS_INTERIOR + GLOB.WALLITEMS_EXTERIOR
//make sure its not in the list of "uncrushable" stuff
if(is_type_in_list(atom_target, vendor_uncrushable_objects))
return FALSE
//check if it has integrity + allow ninjas, etc to be crushed in cloak
if (atom_target.uses_integrity && !(atom_target.invisibility > SEE_INVISIBLE_LIVING))
return TRUE //SMUSH IT
return FALSE
/**
* Causes src to fall onto [target], crushing everything on it (including itself) with [damage]
* and a small chance to do a spectacular effect per entity (if a chance above 0 is provided).
*
* Args:
* * turf/target: The turf to fall onto. Cannot be null.
* * damage: The raw numerical damage to do by default.
* * chance_to_crit: The percent chance of a critical hit occurring. Default: 0
* * forced_crit_case: If given a value from crushing.dm, [target] and its contents will always be hit with that specific critical hit. Default: null
* * paralyze_time: The time, in deciseconds, a given mob/living will be paralyzed for if crushed.
* * crush_dir: The direction the crush is coming from. Default: dir of src to [target].
* * damage_type: The type of damage to do. Default: BRUTE
* * damage_flag: The attack flag for armor purposes. Default: MELEE
* * rotation: The angle of which to rotate src's transform by on a successful tilt. Default: 90.
*
* Returns: A collection of bitflags defined in crushing.dm. Read that file's documentation for info.
*/
/atom/movable/proc/fall_and_crush(turf/target, damage, chance_to_crit = 0, forced_crit_case = null, paralyze_time, crush_dir = get_dir(get_turf(src), target), damage_type = BRUTE, damage_flag = MELEE, rotation = 90)
ASSERT(!isnull(target))
var/flags_to_return = NONE
if (!target.is_blocked_turf(TRUE, src, list(src)))
for(var/atom/atom_target in (target.contents) + target)
if (isarea(atom_target))
continue
if (SEND_SIGNAL(atom_target, COMSIG_PRE_TILT_AND_CRUSH, src) & COMPONENT_IMMUNE_TO_TILT_AND_CRUSH)
continue
var/crit_case = forced_crit_case
if (isnull(crit_case) && chance_to_crit > 0)
if (prob(chance_to_crit))
crit_case = pick_weight(get_crit_crush_chances())
var/crit_rebate_mult = 1 // lessen the normal damage we deal for some of the crits
if (!isnull(crit_case))
crit_rebate_mult = fall_and_crush_crit_rebate_table(crit_case)
apply_crit_crush(crit_case, atom_target)
var/adjusted_damage = damage * crit_rebate_mult
var/crushed
if (isliving(atom_target))
crushed = TRUE
var/mob/living/carbon/living_target = atom_target
var/was_alive = living_target.stat != DEAD
var/blocked = living_target.run_armor_check(attack_flag = damage_flag)
if (iscarbon(living_target))
var/mob/living/carbon/carbon_target = living_target
if(prob(30))
carbon_target.apply_damage(max(0, adjusted_damage), damage_type, blocked = blocked, forced = TRUE, spread_damage = TRUE, attack_direction = crush_dir) // the 30% chance to spread the damage means you escape breaking any bones
else
var/brute = (damage_type == BRUTE ? damage : 0) * 0.5
var/burn = (damage_type == BURN ? damage : 0) * 0.5
carbon_target.take_bodypart_damage(brute, burn, check_armor = TRUE, wound_bonus = 5) // otherwise, deal it to 2 random limbs (or the same one) which will likely shatter something
carbon_target.take_bodypart_damage(brute, burn, check_armor = TRUE, wound_bonus = 5)
carbon_target.AddElement(/datum/element/squish, 80 SECONDS)
else
living_target.apply_damage(adjusted_damage, damage_type, blocked = blocked, forced = TRUE, attack_direction = crush_dir)
living_target.Paralyze(paralyze_time)
living_target.emote("scream")
playsound(living_target, 'sound/effects/blob/blobattack.ogg', 40, TRUE)
playsound(living_target, 'sound/effects/splat.ogg', 50, TRUE)
post_crush_living(living_target, was_alive)
flags_to_return |= (SUCCESSFULLY_CRUSHED_MOB|SUCCESSFULLY_CRUSHED_ATOM)
else if(check_atom_crushable(atom_target))
atom_target.take_damage(adjusted_damage, damage_type, damage_flag, FALSE, crush_dir)
crushed = TRUE
flags_to_return |= SUCCESSFULLY_CRUSHED_ATOM
if (crushed)
atom_target.visible_message(span_danger("[atom_target] is crushed by [src]!"), span_userdanger("You are crushed by [src]!"))
SEND_SIGNAL(atom_target, COMSIG_POST_TILT_AND_CRUSH, src)
var/matrix/to_turn = turn(transform, rotation)
animate(src, transform = to_turn, 0.2 SECONDS)
playsound(src, 'sound/effects/bang.ogg', 40)
visible_message(span_danger("[src] tips over, slamming hard onto [target]!"))
flags_to_return |= SUCCESSFULLY_FELL_OVER
post_tilt()
else
visible_message(span_danger("[src] rebounds comically as it fails to slam onto [target]!"))
Move(target, crush_dir) // we still TRY to move onto it for shit like teleporters
return flags_to_return
/**
* Returns a assoc list of (critcase -> num), where critcase is a critical define in crushing.dm and num is a weight.
* Use with pickweight to acquire a random critcase.
*/
/atom/movable/proc/get_crit_crush_chances()
RETURN_TYPE(/list)
return list(
CRUSH_CRIT_SHATTER_LEGS = 100,
CRUSH_CRIT_PARAPLEGIC = 80,
CRUSH_CRIT_HEADGIB = 20,
CRUSH_CRIT_SQUISH_LIMB = 100
)
/**
* Exists for the purposes of custom behavior.
* Called directly after [crushed] is crushed.
*
* Args:
* * mob/living/crushed: The mob that was crushed.
* * was_alive: Boolean. True if the mob was alive before the crushing.
*/
/atom/movable/proc/post_crush_living(mob/living/crushed, was_alive)
return
/**
* Exists for the purposes of custom behavior.
* Called directly after src actually rotates and falls over.
*/
/atom/movable/proc/post_tilt()
return
/**
* Should be where critcase effects are actually implemented. Use this to apply critcases.
* Args:
* * crit_case: The chosen critcase, defined in crushing.dm.
* * atom/atom_target: The target to apply the critical hit to. Cannot be null. Can be anything except /area.
*
* Returns:
* TRUE if a crit case is successfully applied, FALSE otherwise.
*/
/atom/movable/proc/apply_crit_crush(crit_case, atom/atom_target)
switch (crit_case)
if(CRUSH_CRIT_SHATTER_LEGS) // shatter their legs and bleed 'em
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
carbon_target.bleed(150)
var/obj/item/bodypart/leg/left/left_leg = carbon_target.get_bodypart(BODY_ZONE_L_LEG)
if(left_leg)
left_leg.receive_damage(brute = 200)
var/obj/item/bodypart/leg/right/right_leg = carbon_target.get_bodypart(BODY_ZONE_R_LEG)
if(right_leg)
right_leg.receive_damage(brute = 200)
if(left_leg || right_leg)
carbon_target.visible_message(span_danger("[carbon_target]'s legs shatter with a sickening crunch!"), span_userdanger("Your legs shatter with a sickening crunch!"))
return TRUE
if(CRUSH_CRIT_PARAPLEGIC) // paralyze this binch
// the new paraplegic gets like 4 lines of losing their legs so skip them
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
visible_message(span_danger("[carbon_target]'s spinal cord is obliterated with a sickening crunch!"), ignored_mobs = list(carbon_target))
carbon_target.gain_trauma(/datum/brain_trauma/severe/paralysis/paraplegic)
return TRUE
if(CRUSH_CRIT_SQUISH_LIMB) // limb squish!
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
for(var/obj/item/bodypart/squish_part in carbon_target.get_bodyparts())
var/severity = pick(WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_CRITICAL)
if (!carbon_target.cause_wound_of_type_and_severity(WOUND_BLUNT, squish_part, severity, wound_source = "crushed by [src]"))
squish_part.receive_damage(brute = 30)
carbon_target.visible_message(span_danger("[carbon_target]'s body is maimed underneath the mass of [src]!"), span_userdanger("Your body is maimed underneath the mass of [src]!"))
return TRUE
if(CRUSH_CRIT_HEADGIB) // skull squish!
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
var/obj/item/bodypart/head/carbon_head = carbon_target.get_bodypart(BODY_ZONE_HEAD)
if(carbon_head)
if(carbon_head.dismember())
carbon_target.visible_message(span_danger("[carbon_head] explodes in a shower of gore beneath [src]!"), span_userdanger("Oh f-"))
carbon_head.drop_organs()
qdel(carbon_head)
new /obj/effect/gibspawner/human/bodypartless(get_turf(carbon_target), carbon_target)
return TRUE
return FALSE
/**
* Tilts ontop of the atom supplied, if crit is true some extra shit can happen. See [fall_and_crush] for return values.
* Arguments:
* fatty - atom to tilt the vendor onto
* local_crit_chance - percent chance of a critical hit
* forced_crit - specific critical hit case to use, if any
* range - the range of the machine when thrown if not adjacent
*/
/obj/machinery/vending/proc/tilt(atom/fatty, local_crit_chance = crit_chance, forced_crit, range = 1)
if(QDELETED(src) || !has_gravity(src))
return
. = NONE
var/picked_rotation = pick(90, 270)
if(Adjacent(fatty))
. = fall_and_crush(get_turf(fatty), squish_damage, local_crit_chance, forced_crit, 6 SECONDS, rotation = picked_rotation)
if (. & SUCCESSFULLY_FELL_OVER)
visible_message(span_danger("[src] tips over!"))
tilted = TRUE
tilted_rotation = picked_rotation
layer = ABOVE_MOB_LAYER
if(get_turf(fatty) != get_turf(src))
throw_at(get_turf(fatty), range, 1, spin = FALSE, quickstart = FALSE)
/obj/machinery/vending/post_crush_living(mob/living/crushed, was_alive)
if(was_alive && crushed.stat == DEAD && crushed.client)
crushed.client.give_award(/datum/award/achievement/misc/vendor_squish, crushed) // good job losing a fight with an inanimate object idiot
add_memory_in_range(crushed, 7, /datum/memory/witness_vendor_crush, protagonist = crushed, antagonist = src)
return ..()
/**
* Allows damage to be reduced on certain crit cases.
* Args:
* * crit_case: The critical case chosen.
*/
/atom/movable/proc/fall_and_crush_crit_rebate_table(crit_case)
ASSERT(!isnull(crit_case))
switch(crit_case)
if (CRUSH_CRIT_SHATTER_LEGS)
return 0.2
else
return 1
/obj/machinery/vending/fall_and_crush_crit_rebate_table(crit_case)
return crit_case == VENDOR_CRUSH_CRIT_GLASSCANDY ? 0.33 : ..()
/obj/machinery/vending/get_crit_crush_chances()
return list(
VENDOR_CRUSH_CRIT_GLASSCANDY = 100,
VENDOR_CRUSH_CRIT_PIN = 100
)
/obj/machinery/vending/apply_crit_crush(crit_case, atom_target)
. = ..()
if (.)
return TRUE
switch (crit_case)
if (VENDOR_CRUSH_CRIT_GLASSCANDY)
if (!iscarbon(atom_target))
return FALSE
var/mob/living/carbon/carbon_target = atom_target
for(var/i in 1 to 7)
var/obj/item/shard/shard = new /obj/item/shard(get_turf(carbon_target))
var/datum/embedding/embed = shard.get_embed()
embed.embed_chance = 100
embed.ignore_throwspeed_threshold = TRUE
embed.impact_pain_mult = 1
carbon_target.hitby(shard, skipcatch = TRUE, hitpush = FALSE)
embed.embed_chance = initial(embed.embed_chance)
embed.ignore_throwspeed_threshold = initial(embed.ignore_throwspeed_threshold)
embed.impact_pain_mult = initial(embed.impact_pain_mult)
return TRUE
if (VENDOR_CRUSH_CRIT_PIN) // pin them beneath the machine until someone untilts it
if (!isliving(atom_target))
return FALSE
var/mob/living/living_target = atom_target
forceMove(get_turf(living_target))
buckle_mob(living_target, force=TRUE)
living_target.visible_message(span_danger("[living_target] is pinned underneath [src]!"), span_userdanger("You are pinned down by [src]!"))
return TRUE
return FALSE
/**
* Rights the vendor up, unpinning mobs under it, if any.
* Arguments:
* user - mob that has untilted the vendor
*/
/obj/machinery/vending/proc/untilt(mob/user)
if(user)
user.visible_message(span_notice("[user] rights [src]."), \
span_notice("You right [src]."))
unbuckle_all_mobs(TRUE)
tilted = FALSE
layer = initial(layer)
var/matrix/to_turn = turn(transform, -tilted_rotation)
animate(src, transform = to_turn, 0.2 SECONDS)
tilted_rotation = 0