Files
BloopandGitHub 44478af23c Stops flesh abominations from trying to flail around after they've been deleted (#93725)
## About The Pull Request

This might sound fun but it's really not, all you get is a hard delete +
runtime.

<img width="885" height="396" alt="image"
src="https://github.com/user-attachments/assets/ef43e15e-e54f-4c66-a115-3072bcd35e29"
/>

<img width="785" height="125" alt="image"
src="https://github.com/user-attachments/assets/fa66f0b5-fff7-47eb-b5ce-4e971ac9cbcc"
/>

## Why It's Good For The Game

Bugfix

## Changelog

Not player-facing
2025-11-02 15:42:05 +01: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, times_fired)
. = ..()
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.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, TRUE)
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