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

214 lines
8.4 KiB
Plaintext

/// Chance per second to print a warning text
#define LIVING_FLESH_WARN_CHANCE 3
/// Chance per second to perform an unwanted interaction
#define LIVING_FLESH_INTERFERENCE_CHANCE 1.5
/// Chance to caress instead of grab something nearby without combat mode
#define LIVING_FLESH_TOUCH_CHANCE 30
/// Chance to punch instead of grab something nearby in combat mode
#define LIVING_FLESH_COMBAT_TOUCH_CHANCE 70
/datum/ai_controller/basic_controller/living_limb_flesh
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree
)
/mob/living/basic/living_limb_flesh
name = "living flesh"
desc = "A vaguely leg or arm shaped flesh abomination. It pulses, like a heart."
icon = 'icons/mob/simple/animal.dmi'
icon_state = "limb"
icon_living = "limb"
mob_size = MOB_SIZE_SMALL
basic_mob_flags = DEL_ON_DEATH
faction = list(FACTION_HOSTILE)
melee_damage_lower = 10
melee_damage_upper = 10
health = 20
maxHealth = 20
attack_sound = 'sound/items/weapons/bite.ogg'
attack_vis_effect = ATTACK_EFFECT_BITE
attack_verb_continuous = "tries desperately to attach to"
attack_verb_simple = "try to attach to"
mob_biotypes = MOB_ORGANIC | MOB_SPECIAL
ai_controller = /datum/ai_controller/basic_controller/living_limb_flesh
/// the meat bodypart we are currently inside, used to like drain nutrition and dismember and shit
var/obj/item/bodypart/current_bodypart
/mob/living/basic/living_limb_flesh/Initialize(mapload, obj/item/bodypart/limb)
. = ..()
AddComponent(/datum/component/swarming, max_x = 8, max_y = 8)
AddElement(/datum/element/death_drops, /obj/effect/gibspawner/generic)
if(!isnull(limb))
register_to_limb(limb)
/mob/living/basic/living_limb_flesh/apply_target_randomisation()
AddElement(/datum/element/attack_zone_randomiser, GLOB.limb_zones)
/mob/living/basic/living_limb_flesh/Destroy(force)
. = ..()
if(current_bodypart)
var/obj/item/bodypart/bodypart = current_bodypart
unregister_from_limb(current_bodypart.owner)
if(!QDELETED(bodypart))
qdel(bodypart)
/mob/living/basic/living_limb_flesh/Life(seconds_per_tick = SSMOBS_DT)
. = ..()
if(stat == DEAD)
return
if(isnull(current_bodypart) || isnull(current_bodypart.owner))
return
var/mob/living/carbon/human/victim = current_bodypart.owner
if(SPT_PROB(LIVING_FLESH_WARN_CHANCE, SSMOBS_DT))
to_chat(victim, span_warning("The skin on your [current_bodypart.plaintext_zone] crawls."))
victim.adjust_nutrition(-1.5)
if(!SPT_PROB(LIVING_FLESH_INTERFERENCE_CHANCE, SSMOBS_DT))
return
if(istype(current_bodypart, /obj/item/bodypart/leg))
if(HAS_TRAIT(victim, TRAIT_IMMOBILIZED))
return
step(victim, pick(GLOB.cardinals))
to_chat(victim, span_warning("Your [current_bodypart.plaintext_zone] moves on its own!"))
return
var/list/candidates = list()
for(var/atom/movable/movable in orange(victim, 1))
if(movable == victim)
continue
if(!movable.IsReachableBy(victim) || movable.invisibility > victim.see_invisible)
continue
candidates += movable
if(!length(candidates))
return
var/atom/movable/candidate = pick(candidates)
if(isnull(candidate))
return
if (!prob(victim.combat_mode ? LIVING_FLESH_COMBAT_TOUCH_CHANCE : LIVING_FLESH_TOUCH_CHANCE) && candidate.can_be_pulled(user = victim, force = victim.pull_force))
victim.visible_message(span_warning("[victim]'s [current_bodypart.plaintext_zone] suddenly fastens around [candidate]!"))
INVOKE_ASYNC(victim, TYPE_PROC_REF(/atom/movable, start_pulling), candidate, supress_message = TRUE)
return
victim.visible_message(span_warning("[victim]'s [current_bodypart.plaintext_zone] suddenly spasms towards [candidate]!"))
var/active_hand = victim.active_hand_index
var/new_index = (current_bodypart.body_zone == BODY_ZONE_L_ARM) ? LEFT_HANDS : RIGHT_HANDS
if (active_hand != new_index)
victim.swap_hand(new_index, TRUE)
victim.resolve_unarmed_attack(candidate)
if (active_hand != victim.active_hand_index) // Different check in case we failed to swap hands previously due to holding a bulky item
victim.swap_hand(active_hand, TRUE)
/mob/living/basic/living_limb_flesh/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
. = ..()
if (!ishuman(target) || target.stat == DEAD || HAS_TRAIT(target, TRAIT_NODISMEMBER))
return
var/list/zone_candidates = target.get_missing_limbs()
for(var/obj/item/bodypart/bodypart in target.get_bodyparts())
if(bodypart.body_zone == BODY_ZONE_HEAD || bodypart.body_zone == BODY_ZONE_CHEST)
continue
if(HAS_TRAIT(bodypart, TRAIT_IGNORED_BY_LIVING_FLESH))
continue
if(bodypart.bodypart_flags & BODYPART_UNREMOVABLE)
continue
if(bodypart.brute_dam < 20)
continue
zone_candidates += bodypart.body_zone
if(!length(zone_candidates))
return
var/target_zone = pick(zone_candidates)
var/obj/item/bodypart/target_part = target.get_bodypart(target_zone)
if(isnull(target_part))
target.emote("scream") // dismember already makes them scream so only do this if we aren't doing that
else
target_part.dismember()
var/part_type
switch(target_zone)
if(BODY_ZONE_L_ARM)
part_type = /obj/item/bodypart/arm/left/flesh
if(BODY_ZONE_R_ARM)
part_type = /obj/item/bodypart/arm/right/flesh
if(BODY_ZONE_L_LEG)
part_type = /obj/item/bodypart/leg/left/flesh
if(BODY_ZONE_R_LEG)
part_type = /obj/item/bodypart/leg/right/flesh
if (!isnull(target_part))
target.visible_message(span_danger("[src] tears off [target]'s [target_part.plaintext_zone] and attaches itself in [target_part.p_their()] place!"), span_userdanger("[src] tears off your [target_part.plaintext_zone] and attaches itself in [target_part.p_their()] place!"))
else
target.visible_message(span_danger("[src] attaches itself to where [target]'s [target.parse_zone_with_bodypart(target_zone)] used to be!"), span_userdanger("[src] attaches itself to where your [target.parse_zone_with_bodypart(target_zone)] used to be!"))
var/obj/item/bodypart/new_bodypart = new part_type()
forceMove(new_bodypart)
new_bodypart.replace_limb(target)
register_to_limb(new_bodypart)
/mob/living/basic/living_limb_flesh/proc/owner_shocked(datum/source, shock_damage, shock_source, siemens_coeff, flags)
SIGNAL_HANDLER
if(shock_damage < 10)
return
var/mob/living/carbon/human/part_owner = current_bodypart.owner
if(!detach_self())
return
var/turf/our_location = get_turf(src)
our_location.visible_message(span_warning("[part_owner][part_owner.p_s()] [current_bodypart.plaintext_zone] begins to convulse wildly!"))
/mob/living/basic/living_limb_flesh/proc/owner_died(datum/source, gibbed)
SIGNAL_HANDLER
if(gibbed)
return
addtimer(CALLBACK(src, PROC_REF(detach_self)), 1 SECONDS) //we need new hosts, dead people suck!
/mob/living/basic/living_limb_flesh/proc/detach_self()
if(isnull(current_bodypart))
return FALSE
current_bodypart.dismember()
return TRUE//on_limb_lost should be called after that
/mob/living/basic/living_limb_flesh/proc/on_limb_lost(atom/movable/source, mob/living/carbon/old_owner, special, dismembered)
SIGNAL_HANDLER
unregister_from_limb(old_owner)
addtimer(CALLBACK(src, PROC_REF(wake_up), source), 2 SECONDS, TIMER_STOPPABLE | TIMER_DELETE_ME)
/mob/living/basic/living_limb_flesh/proc/register_to_limb(obj/item/bodypart/part)
current_bodypart = part
ai_controller.set_ai_status(AI_STATUS_OFF)
RegisterSignal(current_bodypart, COMSIG_BODYPART_REMOVED, PROC_REF(on_limb_lost))
if(current_bodypart.owner)
RegisterSignal(current_bodypart.owner, COMSIG_LIVING_DEATH, PROC_REF(owner_died))
RegisterSignal(current_bodypart.owner, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(owner_shocked)) //detach if we are shocked, not beneficial for the host but hey its a sideeffect
/mob/living/basic/living_limb_flesh/proc/unregister_from_limb(mob/living/carbon/removing_owner)
UnregisterSignal(current_bodypart, COMSIG_BODYPART_REMOVED)
if(removing_owner)
UnregisterSignal(removing_owner, COMSIG_LIVING_ELECTROCUTE_ACT)
UnregisterSignal(removing_owner, COMSIG_LIVING_DEATH)
current_bodypart = null
/mob/living/basic/living_limb_flesh/proc/wake_up(atom/limb)
if(QDELETED(src))
return
visible_message(span_warning("[src] begins flailing around!"))
Shake(6, 6, 0.5 SECONDS)
ai_controller.set_ai_status(AI_STATUS_ON)
forceMove(limb.drop_location())
qdel(limb)
#undef LIVING_FLESH_TOUCH_CHANCE
#undef LIVING_FLESH_COMBAT_TOUCH_CHANCE
#undef LIVING_FLESH_WARN_CHANCE
#undef LIVING_FLESH_INTERFERENCE_CHANCE