mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 21:18:37 +01:00
+37






![tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)


Alexis
GitHub
tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
ArcaneMusic
MrMelbert
Rhials
rageguy505
LemonInTheDark
John Willard
Aliceee2ch
Time-Green
Tsar-Salat
SmArtKar
Maxipat
SyncIt21
deltanedas
SimplyLogan
loganuk
Leland Kemble
FalloutFalcon
Roxy
Lucy
siliconOpossum
Isratosh
TheRyeGuyWhoWillNowDie
Neocloudy
Alexander V.
ElGitificador
Twaticus
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bloop
Cameron Lennox
Tim
Iamgoofball
Layzu666
Arturlang
_0Steven
mrmanlikesbt
Ben10Omintrix
John F. Kennedy
Cursor
Josh
Josh Powell
Yobrocharlie
Hardly3D
shayoki
LT3
21b4095dfd
Upstream 04/17/2026 fixes https://github.com/Bubberstation/Bubberstation/issues/5549 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com> Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com> Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com> Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com> Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com> Co-authored-by: loganuk <fakeemail123@aol.com> Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com> Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com> Co-authored-by: Lucy <lucy@absolucy.moe> Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com> Co-authored-by: Isratosh <Isratosh@hotmail.com> Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com> Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com> Co-authored-by: Alexander V. <volas@ya.ru> Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com> Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com> Co-authored-by: Cameron Lennox <killer65311@gmail.com> Co-authored-by: Tim <timothymtorres@gmail.com> Co-authored-by: Iamgoofball <iamgoofball@gmail.com> Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com> Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com> Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com> Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com> Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com> Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com> Co-authored-by: Josh <josh.adam.powell@gmail.com> Co-authored-by: Josh Powell <josh.powell@softwire.com> Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com> Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com> Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com> Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
586 lines
21 KiB
Plaintext
586 lines
21 KiB
Plaintext
/datum/component/butchering
|
|
/// Time in deciseconds taken to butcher something
|
|
var/speed = 8 SECONDS
|
|
/// Percentage effectiveness; numbers above 100 yield extra drops
|
|
var/effectiveness = 100
|
|
/// Percentage increase to bonus item chance
|
|
var/bonus_modifier = 0
|
|
/// Sound played when butchering
|
|
var/butcher_sound = 'sound/effects/butcher.ogg'
|
|
/// Whether or not this component can be used to butcher currently. Used to temporarily disable butchering
|
|
var/butchering_enabled = TRUE
|
|
/// Whether or not this component is compatible with blunt tools.
|
|
var/can_be_blunt = FALSE
|
|
/// Callback for butchering
|
|
var/datum/callback/butcher_callback
|
|
|
|
/datum/component/butchering/Initialize(
|
|
speed = 8 SECONDS,
|
|
effectiveness = 100,
|
|
bonus_modifier = 0,
|
|
butcher_sound = 'sound/effects/butcher.ogg',
|
|
disabled = FALSE,
|
|
can_be_blunt = FALSE,
|
|
butcher_callback,
|
|
)
|
|
src.speed = speed
|
|
src.effectiveness = effectiveness
|
|
src.bonus_modifier = bonus_modifier
|
|
src.butcher_sound = butcher_sound
|
|
if (disabled)
|
|
src.butchering_enabled = FALSE
|
|
src.can_be_blunt = can_be_blunt
|
|
src.butcher_callback = butcher_callback
|
|
|
|
/datum/component/butchering/Destroy(force)
|
|
butcher_callback = null
|
|
return ..()
|
|
|
|
/datum/component/butchering/RegisterWithParent()
|
|
if (!isitem(parent))
|
|
return
|
|
var/obj/item/item_parent = parent
|
|
item_parent.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS
|
|
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
|
|
RegisterSignal(parent, COMSIG_ITEM_INTERACTING_WITH_ATOM, PROC_REF(on_item_interaction))
|
|
RegisterSignal(parent, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context))
|
|
|
|
/datum/component/butchering/UnregisterFromParent()
|
|
UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK, COMSIG_ITEM_INTERACTING_WITH_ATOM))
|
|
|
|
/datum/component/butchering/proc/on_item_attack(obj/item/source, mob/living/victim, mob/living/user, list/modifiers)
|
|
SIGNAL_HANDLER
|
|
|
|
if (!source.get_sharpness() && !can_be_blunt)
|
|
return
|
|
|
|
if (!user.combat_mode)
|
|
return
|
|
|
|
// Can we butcher it?
|
|
if (victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results) && butchering_enabled)
|
|
INVOKE_ASYNC(src, PROC_REF(start_butcher), source, victim, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
if (!ishuman(victim) || !source.force)
|
|
return
|
|
|
|
if (LAZYACCESS(modifiers, RIGHT_CLICK) && butchering_enabled)
|
|
INVOKE_ASYNC(src, PROC_REF(butcher_human), source, victim, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
// Neckslicing requires aggro grabs
|
|
if (user.pulling != victim || user.grab_state < GRAB_AGGRESSIVE || user.zone_selected != BODY_ZONE_HEAD)
|
|
return
|
|
|
|
if (HAS_TRAIT(user, TRAIT_PACIFISM))
|
|
to_chat(user, span_warning("You don't want to harm other living beings!"))
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
if (victim.has_status_effect(/datum/status_effect/neck_slice))
|
|
return
|
|
|
|
INVOKE_ASYNC(src, PROC_REF(start_neck_slice), source, victim, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
/datum/component/butchering/proc/on_item_interaction(obj/item/source, mob/living/user, atom/target)
|
|
SIGNAL_HANDLER
|
|
|
|
if (!source.get_sharpness() && !can_be_blunt)
|
|
return
|
|
|
|
if (!user.combat_mode || !butchering_enabled)
|
|
return
|
|
|
|
if (isbodypart(target))
|
|
INVOKE_ASYNC(src, PROC_REF(butcher_limb), source, target, user)
|
|
return COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
/datum/component/butchering/proc/add_item_context(obj/item/source, list/context, atom/target, mob/living/user)
|
|
SIGNAL_HANDLER
|
|
|
|
if (!source.get_sharpness() && !can_be_blunt)
|
|
return NONE
|
|
|
|
if (!butchering_enabled)
|
|
return NONE
|
|
|
|
if (isbodypart(target))
|
|
context[SCREENTIP_CONTEXT_LMB] = "(Combat Mode) Butcher limb"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
if (!isliving(target))
|
|
return NONE
|
|
|
|
var/mob/living/victim = target
|
|
if (victim.stat != DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
|
|
context[SCREENTIP_CONTEXT_LMB] = "(Combat Mode) Butcher"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
if (ishuman(victim))
|
|
context[SCREENTIP_CONTEXT_RMB] = "(Combat Mode) Butcher"
|
|
return CONTEXTUAL_SCREENTIP_SET
|
|
|
|
return NONE
|
|
|
|
/datum/component/butchering/proc/butcher_limb(obj/item/source, obj/item/bodypart/target, mob/living/user)
|
|
target.add_fingerprint(user)
|
|
if (LIMB_HAS_SKIN(target) && !HAS_ANY_SURGERY_STATE(target.surgery_state, SURGERY_SKIN_CUT | SURGERY_SKIN_OPEN))
|
|
to_chat(user, span_warning("[target]'s skin is still intact!"))
|
|
return
|
|
|
|
if (LIMB_HAS_BONES(target) && !HAS_ANY_SURGERY_STATE(target.surgery_state, SURGERY_BONE_DRILLED | SURGERY_BONE_SAWED))
|
|
// We need to gut the limb before turning it into meat, otherwise just cut around the bone I guess
|
|
if (length(target.contents))
|
|
to_chat(user, span_warning("[target]'s bones are still intact!"))
|
|
return
|
|
|
|
var/speed_modifier = 1
|
|
if (!target.owner)
|
|
speed_modifier = 0.5
|
|
else if (target.owner.stat < UNCONSCIOUS)
|
|
speed_modifier = 1.5 // yeowch
|
|
|
|
var/limb_descriptor = (target.owner ? "[target.owner]'s [target.plaintext_zone]" : target)
|
|
// Okay *hopefully* they're already dead at this point
|
|
if (target.body_zone == BODY_ZONE_CHEST && target.owner)
|
|
// Butchering the chest gibs the victim
|
|
limb_descriptor = target.owner
|
|
|
|
if (target.owner)
|
|
log_combat(user, target.owner, "attempted to butcher", source)
|
|
|
|
if (length(target.contents))
|
|
user.visible_message(span_warning("[user] begins to gut [limb_descriptor]!"), span_notice("You begin to gut [limb_descriptor]..."), ignored_mobs = target.owner)
|
|
if (target.owner)
|
|
to_chat(target.owner, span_warning("[user] begins to gut your [target.plaintext_zone]!"))
|
|
|
|
playsound(target.loc, butcher_sound, 50, TRUE, -1)
|
|
if (!do_after(user, speed * speed_modifier, target.owner || target))
|
|
return
|
|
target.drop_organs(user, TRUE)
|
|
return
|
|
|
|
if (!length(target.butcher_drops))
|
|
to_chat(user, span_warning("There is nothing left inside [limb_descriptor]!"))
|
|
return
|
|
|
|
if (target.body_zone == BODY_ZONE_CHEST && target.owner)
|
|
// Cannot butcher the chest until we hack off all the other limbs
|
|
for (var/obj/item/bodypart/limb as anything in target.owner.get_bodyparts())
|
|
if (limb != target && limb.butcher_drops && limb.butcher_replacement)
|
|
to_chat(user, span_warning("You need to butcher all other limbs first!"))
|
|
return
|
|
|
|
user.visible_message(span_warning("[user] begins to cut [limb_descriptor] apart!"), span_notice("You begin to cut [limb_descriptor] apart..."), ignored_mobs = target.owner)
|
|
if (target.owner)
|
|
to_chat(target.owner, span_warning("[user] begins to cut your [target.plaintext_zone] apart!"))
|
|
|
|
playsound(target.loc, butcher_sound, 50, TRUE, -1)
|
|
if (!do_after(user, speed * speed_modifier, target.owner || target))
|
|
return
|
|
|
|
var/list/results = list()
|
|
var/turf/drop_loc = target.owner?.drop_location() || target.drop_location()
|
|
var/bonus_chance = max(0, (effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
|
|
|
|
var/list/failures = list()
|
|
var/list/bonuses = list()
|
|
|
|
for (var/obj/item/drop_type as anything in target.butcher_drops)
|
|
var/amount = target.butcher_drops[drop_type] || 1
|
|
var/is_stack = ispath(drop_type, /obj/item/stack)
|
|
|
|
for (var/i in 1 to amount)
|
|
if (!prob(effectiveness))
|
|
failures |= drop_type::name
|
|
amount -= 1
|
|
continue
|
|
|
|
if (prob(bonus_chance))
|
|
if (!is_stack)
|
|
if (ispath(drop_type, /obj/item/food/meat))
|
|
results += new drop_type(drop_loc, target.blood_dna_info?.Copy())
|
|
else
|
|
var/obj/item/butcher_result = new drop_type(drop_loc)
|
|
if (target.blood_dna_info)
|
|
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
|
|
results += butcher_result
|
|
amount += 1
|
|
bonuses |= drop_type::name
|
|
|
|
if (is_stack)
|
|
continue
|
|
|
|
if (ispath(drop_type, /obj/item/food/meat))
|
|
results += new drop_type(drop_loc, target.blood_dna_info?.Copy())
|
|
continue
|
|
|
|
var/obj/item/butcher_result = new drop_type(drop_loc)
|
|
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
|
|
results += butcher_result
|
|
|
|
if (is_stack && amount)
|
|
var/obj/item/stack/butcher_result = null
|
|
if (ispath(drop_type, /obj/item/stack/sheet/animalhide/carbon))
|
|
butcher_result = new drop_type(drop_loc, amount, /*merge = */TRUE, /*mat_override = */null, /*mat_amount = */1, target.skin_tone || target.species_color)
|
|
else
|
|
butcher_result = new drop_type(drop_loc, amount)
|
|
if (target.blood_dna_info)
|
|
butcher_result.add_blood_DNA(target.blood_dna_info.Copy())
|
|
results += butcher_result
|
|
|
|
if (target.reagents)
|
|
var/meat_produced = 0
|
|
for (var/obj/item/food/target_meat in results)
|
|
meat_produced += 1
|
|
|
|
for (var/obj/item/food/target_meat in results)
|
|
target.reagents.trans_to(target_meat, target.reagents.total_volume / meat_produced, remove_blacklisted = TRUE)
|
|
|
|
if (target.owner)
|
|
var/reagents_in_produced = 0
|
|
for(var/obj/item/result as anything in results)
|
|
if(result.reagents)
|
|
reagents_in_produced += 1
|
|
|
|
var/list/diseases = target.owner.get_static_viruses()
|
|
|
|
for(var/obj/item/result as anything in results)
|
|
if (reagents_in_produced)
|
|
if (target.owner.reagents)
|
|
target.owner.reagents.trans_to(result, target.owner.reagents.total_volume / reagents_in_produced / length(target.owner.get_bodyparts()), remove_blacklisted = TRUE)
|
|
result.reagents?.add_reagent(/datum/reagent/consumable/nutriment/fat, target.owner.nutrition / /datum/reagent/consumable/nutriment/fat::nutriment_factor / reagents_in_produced)
|
|
|
|
if(LAZYLEN(diseases))
|
|
var/list/datum/disease/diseases_to_add = list()
|
|
for(var/datum/disease/disease as anything in diseases)
|
|
// Admin or special viruses that should not be reproduced
|
|
if(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS))
|
|
continue
|
|
|
|
diseases_to_add += disease
|
|
|
|
if(LAZYLEN(diseases_to_add))
|
|
result.AddComponent(/datum/component/infective, diseases_to_add)
|
|
|
|
for (var/obj/item/food/meat/meat in results)
|
|
meat.name = "[target.owner.real_name]'s [meat.name]"
|
|
meat.set_custom_materials(list(SSmaterials.get_material(/datum/material/meat/mob_meat, target.owner) = 4 * SHEET_MATERIAL_AMOUNT))
|
|
meat.subjectname = target.owner.real_name
|
|
meat.subjectjob = target.owner.job
|
|
|
|
user.visible_message(span_warning("[user] butchers [limb_descriptor]!"), span_notice("You butcher [limb_descriptor]."), ignored_mobs = target.owner)
|
|
if (!target.owner)
|
|
target.drop_organs(violent_removal = TRUE) // Should not happen, but just in case
|
|
create_replacement_limb(target, drop_loc)
|
|
qdel(target)
|
|
return
|
|
|
|
var/wound_type = null
|
|
if (source.damtype == BURN)
|
|
wound_type = WOUND_BURN
|
|
else
|
|
switch (source.get_sharpness())
|
|
if (SHARP_EDGED)
|
|
wound_type = WOUND_SLASH
|
|
if (SHARP_POINTY)
|
|
wound_type = WOUND_PIERCE
|
|
else
|
|
wound_type = WOUND_BLUNT
|
|
|
|
to_chat(target.owner, span_userdanger("[user] hacks the meat off your [target.plaintext_zone]!"))
|
|
var/mob/living/carbon/victim = target.owner
|
|
|
|
if (!target.butcher_replacement)
|
|
target.dismember(source.damtype, wound_type)
|
|
target.drop_organs(violent_removal = TRUE) // Should not happen, but just in case
|
|
qdel(target)
|
|
return
|
|
|
|
var/obj/item/bodypart/replacement = create_replacement_limb(target, drop_loc)
|
|
target.dismember(source.damtype, wound_type)
|
|
target.drop_organs(violent_removal = TRUE)
|
|
replacement.replace_limb(victim)
|
|
replacement.update_limb(is_creating = TRUE)
|
|
qdel(target)
|
|
|
|
/// Creates a replacement (usually skeleton) limb for the butchered one
|
|
/datum/component/butchering/proc/create_replacement_limb(obj/item/bodypart/target, drop_loc)
|
|
var/drop_type = target.butcher_replacement
|
|
var/obj/item/bodypart/replacement = new drop_type(drop_loc)
|
|
replacement.bodyshape = target.bodyshape
|
|
replacement.set_initial_damage(target.brute_dam, target.burn_dam)
|
|
if (IS_ORGANIC_LIMB(replacement) && target.owner)
|
|
replacement.blood_dna_info = target.owner.get_blood_dna_list()
|
|
|
|
for (var/datum/wound/wound as anything in target.wounds)
|
|
wound.remove_wound()
|
|
wound.apply_wound(replacement, silent = TRUE)
|
|
|
|
return replacement
|
|
|
|
/datum/component/butchering/proc/start_butcher(obj/item/source, mob/living/target, mob/living/user)
|
|
to_chat(user, span_notice("You begin to butcher [target]..."))
|
|
playsound(target.loc, butcher_sound, 50, TRUE, -1)
|
|
if (do_after(user, speed, target) && target.Adjacent(source))
|
|
on_butchering(user, target)
|
|
|
|
/datum/component/butchering/proc/butcher_human(obj/item/source, mob/living/carbon/human/victim, mob/living/user)
|
|
if (DOING_INTERACTION_WITH_TARGET(user, victim))
|
|
to_chat(user, span_warning("You're already interacting with [victim]!"))
|
|
return
|
|
|
|
var/static/list/butcher_spots = typecacheof(list(
|
|
/obj/structure/table,
|
|
/obj/structure/bed,
|
|
/obj/machinery/stasis,
|
|
/obj/structure/kitchenspike,
|
|
))
|
|
|
|
var/found_spot = FALSE
|
|
for (var/obj/thing in victim.loc)
|
|
if (is_type_in_typecache(thing, butcher_spots))
|
|
found_spot = TRUE
|
|
break
|
|
|
|
if (!found_spot)
|
|
to_chat(user, span_warning("You need a better spot to butcher [victim]!"))
|
|
return
|
|
|
|
var/obj/item/bodypart/limb = victim.get_bodypart(deprecise_zone(user.zone_selected))
|
|
if (!limb)
|
|
to_chat(user, span_warning("[victim] doesn't have a [parse_zone(deprecise_zone(user.zone_selected))]!"))
|
|
return
|
|
|
|
butcher_limb(source, limb, user)
|
|
|
|
/datum/component/butchering/proc/start_neck_slice(obj/item/source, mob/living/carbon/human/victim, mob/living/user)
|
|
if (DOING_INTERACTION_WITH_TARGET(user, victim))
|
|
to_chat(user, span_warning("You're already interacting with [victim]!"))
|
|
return
|
|
|
|
user.visible_message(span_danger("[user] is slitting [victim]'s throat!"), \
|
|
span_danger("You start slicing [victim]'s throat!"), \
|
|
span_hear("You hear a cutting noise!"), ignored_mobs = victim)
|
|
victim.show_message(span_userdanger("Your throat is being slit by [user]!"), MSG_VISUAL, \
|
|
span_userdanger("Something is cutting into your neck!"), NONE)
|
|
log_combat(user, victim, "attempted throat slitting", source)
|
|
|
|
playsound(victim.loc, butcher_sound, 50, TRUE, -1)
|
|
if (!do_after(user, clamp(500 / source.force, 30, 100), victim) && victim.Adjacent(source))
|
|
return
|
|
|
|
if (victim.has_status_effect(/datum/status_effect/neck_slice))
|
|
user.show_message(span_warning("[victim]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
|
|
span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
|
|
return
|
|
|
|
victim.visible_message(span_danger("[user] slits [victim]'s throat!"), \
|
|
span_userdanger("[user] slits your throat..."))
|
|
log_combat(user, victim, "wounded via throat slitting", source)
|
|
victim.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD, wound_bonus=CANT_WOUND) // easy tiger, we'll get to that in a sec
|
|
var/obj/item/bodypart/slit_throat = victim.get_bodypart(BODY_ZONE_HEAD)
|
|
if (victim.cause_wound_of_type_and_severity(WOUND_SLASH, slit_throat, WOUND_SEVERITY_CRITICAL))
|
|
victim.apply_status_effect(/datum/status_effect/neck_slice)
|
|
|
|
/**
|
|
* Handles a user butchering a target
|
|
*
|
|
* Arguments:
|
|
* - [butcher][/mob/living]: The mob doing the butchering
|
|
* - [target][/mob/living]: The mob being butchered
|
|
*/
|
|
/datum/component/butchering/proc/on_butchering(atom/butcher, mob/living/target)
|
|
var/list/results = list()
|
|
var/turf/location = target.drop_location()
|
|
var/final_effectiveness = effectiveness - target.butcher_difficulty
|
|
var/bonus_chance = max(0, (final_effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
|
|
|
|
if (target.flags_1 & HOLOGRAM_1)
|
|
butcher.visible_message(span_notice("[butcher] tries to butcher [target], but it vanishes."), \
|
|
span_notice("You try to butcher [target], but it vanishes."))
|
|
qdel(target)
|
|
return
|
|
|
|
var/list/failures = list()
|
|
var/list/bonuses = list()
|
|
for (var/obj/remains as anything in target.butcher_results)
|
|
var/amount = target.butcher_results[remains] || 1
|
|
var/is_stack = ispath(remains, /obj/item/stack)
|
|
|
|
for (var/i in 1 to amount)
|
|
if (!prob(final_effectiveness))
|
|
failures |= remains::name
|
|
amount -= 1
|
|
continue
|
|
|
|
if (prob(bonus_chance))
|
|
if (!is_stack)
|
|
results += new remains(location)
|
|
amount += 1
|
|
bonuses |= remains::name
|
|
|
|
if (!is_stack)
|
|
results += new remains(location)
|
|
|
|
if (is_stack && amount)
|
|
results += new remains(location, amount)
|
|
|
|
target.butcher_results?.Cut()
|
|
|
|
if (butcher)
|
|
if (length(failures))
|
|
to_chat(butcher, span_warning("You fail to harvest some of the [english_list(failures)] from [target]."))
|
|
if (length(bonuses))
|
|
to_chat(butcher, span_info("You harvest some extra [english_list(bonuses)] from [target]!"))
|
|
|
|
for (var/obj/guaranteed_remains as anything in target.guaranteed_butcher_results)
|
|
var/amount = target.guaranteed_butcher_results[guaranteed_remains]
|
|
if (ispath(guaranteed_remains, /obj/item/stack))
|
|
results += new guaranteed_remains(location, amount)
|
|
continue
|
|
|
|
for (var/i in 1 to amount)
|
|
results += new guaranteed_remains(location)
|
|
|
|
target.guaranteed_butcher_results?.Cut()
|
|
|
|
for (var/obj/item/carrion in results)
|
|
var/list/meat_mats = carrion.has_material_type(/datum/material/meat)
|
|
if (!length(meat_mats))
|
|
continue
|
|
carrion.set_custom_materials((carrion.custom_materials - meat_mats) + list(SSmaterials.get_material(/datum/material/meat/mob_meat, target) = counterlist_sum(meat_mats)))
|
|
|
|
// Transfer delicious reagents to meat
|
|
if (target.reagents)
|
|
var/meat_produced = 0
|
|
for (var/obj/item/food/target_meat in results)
|
|
meat_produced += 1
|
|
for (var/obj/item/food/target_meat in results)
|
|
target.reagents.trans_to(target_meat, target.reagents.total_volume / meat_produced, remove_blacklisted = TRUE)
|
|
|
|
// Don't forget yummy diseases either!
|
|
if (iscarbon(target))
|
|
var/mob/living/carbon/host_target = target
|
|
var/list/diseases = host_target.get_static_viruses()
|
|
if (LAZYLEN(diseases))
|
|
var/list/datum/disease/diseases_to_add = list()
|
|
for (var/datum/disease/disease as anything in diseases)
|
|
// admin or special viruses that should not be reproduced
|
|
if (!(disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS)))
|
|
diseases_to_add += disease
|
|
|
|
if (LAZYLEN(diseases_to_add))
|
|
for (var/obj/diseased_remains in results)
|
|
diseased_remains.AddComponent(/datum/component/infective, diseases_to_add)
|
|
|
|
if (butcher)
|
|
butcher.visible_message(span_notice("[butcher] butchers [target]."), \
|
|
span_notice("You butcher [target]."))
|
|
butcher_callback?.Invoke(butcher, target)
|
|
target.harvest(butcher)
|
|
target.log_message("has been butchered by [key_name(butcher)]", LOG_ATTACK)
|
|
target.gib(DROP_BRAIN|DROP_ORGANS)
|
|
|
|
/// Special snowflake component only used for the recycler.
|
|
/datum/component/butchering/recycler
|
|
|
|
/datum/component/butchering/recycler/Initialize(
|
|
speed,
|
|
effectiveness,
|
|
bonus_modifier,
|
|
butcher_sound,
|
|
disabled,
|
|
can_be_blunt,
|
|
butcher_callback,
|
|
)
|
|
if (!istype(parent, /obj/machinery/recycler)) //EWWW
|
|
return COMPONENT_INCOMPATIBLE
|
|
. = ..()
|
|
if (. == COMPONENT_INCOMPATIBLE)
|
|
return
|
|
|
|
var/static/list/loc_connections = list(
|
|
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
|
|
)
|
|
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
|
|
|
|
/datum/component/butchering/recycler/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
|
|
SIGNAL_HANDLER
|
|
|
|
if (!isliving(arrived))
|
|
return
|
|
var/mob/living/victim = arrived
|
|
var/obj/machinery/recycler/eater = parent
|
|
if (eater.safety_mode || (eater.machine_stat & (BROKEN|NOPOWER))) //I'm so sorry.
|
|
return
|
|
if (victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
|
|
on_butchering(parent, victim)
|
|
|
|
/datum/component/butchering/mecha
|
|
|
|
/datum/component/butchering/mecha/RegisterWithParent()
|
|
. = ..()
|
|
RegisterSignal(parent, COMSIG_MECHA_EQUIPMENT_ATTACHED, PROC_REF(enable_butchering))
|
|
RegisterSignal(parent, COMSIG_MECHA_EQUIPMENT_DETACHED, PROC_REF(disable_butchering))
|
|
RegisterSignal(parent, COMSIG_MECHA_DRILL_MOB, PROC_REF(on_drill))
|
|
|
|
/datum/component/butchering/mecha/UnregisterFromParent()
|
|
. = ..()
|
|
UnregisterSignal(parent, list(
|
|
COMSIG_MECHA_DRILL_MOB,
|
|
COMSIG_MECHA_EQUIPMENT_ATTACHED,
|
|
COMSIG_MECHA_EQUIPMENT_DETACHED,
|
|
))
|
|
|
|
/// Enables the butchering mechanic for the mecha who has equipped us.
|
|
/datum/component/butchering/mecha/proc/enable_butchering(datum/source)
|
|
SIGNAL_HANDLER
|
|
butchering_enabled = TRUE
|
|
|
|
/// Disables the butchering mechanic for the mecha who has dropped us.
|
|
/datum/component/butchering/mecha/proc/disable_butchering(datum/source)
|
|
SIGNAL_HANDLER
|
|
butchering_enabled = FALSE
|
|
|
|
///When we are ready to drill through a mob
|
|
/datum/component/butchering/mecha/proc/on_drill(datum/source, obj/vehicle/sealed/mecha/chassis, mob/living/target)
|
|
SIGNAL_HANDLER
|
|
INVOKE_ASYNC(src, PROC_REF(on_butchering), chassis, target)
|
|
|
|
/datum/component/butchering/wearable
|
|
|
|
/datum/component/butchering/wearable/RegisterWithParent()
|
|
. = ..()
|
|
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(worn_enable_butchering))
|
|
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(worn_disable_butchering))
|
|
|
|
/datum/component/butchering/wearable/UnregisterFromParent()
|
|
. = ..()
|
|
UnregisterSignal(parent, list(
|
|
COMSIG_ITEM_EQUIPPED,
|
|
COMSIG_ITEM_DROPPED,
|
|
))
|
|
|
|
/// Same as enable_butchering but for worn items
|
|
/datum/component/butchering/wearable/proc/worn_enable_butchering(obj/item/source, mob/user, slot)
|
|
SIGNAL_HANDLER
|
|
//check if the item is being not worn
|
|
if (!(slot & source.slot_flags))
|
|
return
|
|
butchering_enabled = TRUE
|
|
RegisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(butcher_target))
|
|
|
|
/// Same as disable_butchering but for worn items
|
|
/datum/component/butchering/wearable/proc/worn_disable_butchering(obj/item/source, mob/user)
|
|
SIGNAL_HANDLER
|
|
butchering_enabled = FALSE
|
|
UnregisterSignal(user, COMSIG_LIVING_UNARMED_ATTACK)
|
|
|
|
/datum/component/butchering/wearable/proc/butcher_target(mob/user, atom/target, proximity)
|
|
SIGNAL_HANDLER
|
|
if (!isliving(target))
|
|
return NONE
|
|
return on_item_attack(parent, target, user)
|