From 4d32af150840f0f0322b2cfcfed896cfe424a4bb Mon Sep 17 00:00:00 2001 From: archbtw <230859540+archbtw0@users.noreply.github.com> Date: Sat, 6 Dec 2025 00:29:52 +0000 Subject: [PATCH] Biodegrade dissolves all restraints instead of one single restraint; including grabs (#94135) ## About The Pull Request @Metekillot Continues https://github.com/tgstation/tgstation/pull/92053 https://github.com/tgstation/tgstation/pull/92442 https://github.com/tgstation/tgstation/pull/92911 Biodegrade destroys all restraints instead of one single restraint. Biodegrade will now dissolve all* restraints (fixes niche use-cases such as strait jackets, cuffed prisoner shoes, etc). Biodegrade includes grabs (unless it's another ling): The manhandler gets splashed acid, dealing some damage (this damage is not that bad); the damage dealt depends on BIO protection (for reference - the default security uniform halves the damage). They will also be stunned briefly to give the ling a short chance to leg it. ## Why It's Good For The Game Same reasons as Metek's This change is meant to bring changelings in line with their categorized "uncontainable" valid salad demarcation. Biodegrade kinda sucks, and this makes it not-suck. Makes biodegrade a more attractive ability for changeling by significantly increasing its use as a method to escape confinement of any sort. Hopefully it has the gumption to close up cheesy edge cases as well. ## Changelog :cl: ArchBTW, Bisar balance: Changeling's Biodegrade works against grabs. balance: Changeling's Biodegrade destroys all restraints instead of one single restraint. fix: Changeling's Biodegrade shows the effect when used inside lockers. fix: Changeling's Biodegrade properly covers restraint types (cuffed prisoner shoes, strait jackets, etc). /:cl: --------- Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com> Co-authored-by: ArchBTW <230859540+glue0000@users.noreply.github.com> Co-authored-by: Aki Ito <11748095+ExcessiveUseOfCobblestone@users.noreply.github.com> --- .../changeling/powers/_biodegrade_bioacid.dm | 25 +++ .../changeling/powers/biodegrade.dm | 184 +++++++++--------- code/modules/mob/mob.dm | 2 +- tgstation.dme | 1 + 4 files changed, 115 insertions(+), 97 deletions(-) create mode 100644 code/modules/antagonists/changeling/powers/_biodegrade_bioacid.dm diff --git a/code/modules/antagonists/changeling/powers/_biodegrade_bioacid.dm b/code/modules/antagonists/changeling/powers/_biodegrade_bioacid.dm new file mode 100644 index 00000000000..7278a16c89a --- /dev/null +++ b/code/modules/antagonists/changeling/powers/_biodegrade_bioacid.dm @@ -0,0 +1,25 @@ +/datum/reagent/toxin/acid/bio_acid + name = "adaptive bio-acid" + description = "An immensely strong, acidic substance of seemingly biological origin. It is teeming with microscopic\ + organisms that seem to alter its composition to most adaptively dissolve whatever it comes into contact with." + color = "#9455ff" + creation_purity = 100 + toxpwr = 0 + acidpwr = 0 + ph = 0.0 + penetrates_skin = TOUCH + +/datum/reagent/toxin/acid/bio_acid/expose_mob(mob/living/exposed_mob, methods = TOUCH, reac_volume, show_message = TRUE, touch_protection) + if(IS_CHANGELING(exposed_mob)) + to_chat(exposed_mob, span_changeling("We excrete a bio-agent to neutralize the bio-acid. It is routine and reflexive to do so.")) + volume = min(0.1, volume) + holder.update_total() + return + . = ..() + exposed_mob.adjust_fire_loss(round(reac_volume * min(1 - touch_protection), 0.1) * 3, required_bodytype = BODYTYPE_ORGANIC) // full bio protection = 100% damage reduction + exposed_mob.acid_act(10, 50) + +/datum/reagent/toxin/acid/bio_acid/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) + . = ..() + if(SPT_PROB(50, seconds_per_tick)) + affected_mob.emote(pick("screech", "cry")) diff --git a/code/modules/antagonists/changeling/powers/biodegrade.dm b/code/modules/antagonists/changeling/powers/biodegrade.dm index 8a5fae3bd8a..4a49035d183 100644 --- a/code/modules/antagonists/changeling/powers/biodegrade.dm +++ b/code/modules/antagonists/changeling/powers/biodegrade.dm @@ -3,114 +3,106 @@ desc = "Dissolves restraints or other objects preventing free movement. Costs 30 chemicals." helptext = "This is obvious to nearby people, and can destroy standard restraints and closets." button_icon_state = "biodegrade" - chemical_cost = 30 //High cost to prevent spam + chemical_cost = 30 dna_cost = 2 req_human = TRUE disabled_by_fire = FALSE + var/static/bio_acid_path = /datum/reagent/toxin/acid/bio_acid + var/static/bio_acid_amount_per_spray = 6 + var/static/bio_acid_color = "#9455ff" /datum/action/changeling/biodegrade/sting_action(mob/living/carbon/human/user) - if(user.handcuffed) - var/obj/O = user.get_item_by_slot(ITEM_SLOT_HANDCUFFED) - if(!istype(O)) - return FALSE - user.visible_message(span_warning("[user] vomits a glob of acid on [user.p_their()] [O]!"), \ - span_warning("We vomit acidic ooze onto our restraints!")) + . = FALSE + var/list/obj/restraints = list() + var/obj/handcuffs = user.get_item_by_slot(ITEM_SLOT_HANDCUFFED) + var/obj/legcuffs = user.get_item_by_slot(ITEM_SLOT_LEGCUFFED) + var/obj/item/clothing/suit/straitjacket = user.get_item_by_slot(ITEM_SLOT_OCLOTHING) + var/obj/item/clothing/shoes/sneakers/orange/prisoner_shoes = user.get_item_by_slot(ITEM_SLOT_FEET) + var/obj/item/clothing/shoes/knotted_shoes = user.get_item_by_slot(ITEM_SLOT_FEET) + var/obj/some_manner_of_cage = astype(user.loc, /obj) + var/mob/living/space_invader = user.pulledby - addtimer(CALLBACK(src, PROC_REF(dissolve_handcuffs), user, O), 3 SECONDS) - log_combat(user, user.handcuffed, "melted handcuffs", addition = "(biodegrade)") - ..() - return TRUE + if(!istype(prisoner_shoes, /obj/item/clothing/shoes/sneakers/orange) || !prisoner_shoes?.attached_cuffs) + prisoner_shoes = null + if(!istype(knotted_shoes) || istype(knotted_shoes) && !knotted_shoes.tied == SHOES_KNOTTED) + knotted_shoes = null + if(!straitjacket?.breakouttime) + straitjacket = null - if(user.legcuffed) - var/obj/O = user.get_item_by_slot(ITEM_SLOT_LEGCUFFED) - if(!istype(O)) - return FALSE - user.visible_message(span_warning("[user] vomits a glob of acid on [user.p_their()] [O]!"), \ - span_warning("We vomit acidic ooze onto our restraints!")) + if(!handcuffs && !legcuffs && !straitjacket && !prisoner_shoes && !knotted_shoes && !some_manner_of_cage && !space_invader) + user.balloon_alert(user, "already free!") + return . + ..() - addtimer(CALLBACK(src, PROC_REF(dissolve_legcuffs), user, O), 3 SECONDS) - log_combat(user, user.legcuffed, "melted legcuffs", addition = "(biodegrade)") - ..() - return TRUE + if(handcuffs) + restraints.Add(handcuffs) + if(legcuffs) + restraints.Add(legcuffs) + if(straitjacket) + restraints.Add(straitjacket) + if(prisoner_shoes) + restraints.Add(prisoner_shoes) + if(knotted_shoes) + restraints.Add(knotted_shoes) + if(some_manner_of_cage) + restraints.Add(some_manner_of_cage) - if(user.wear_suit?.breakouttime) - var/obj/item/clothing/suit/S = user.get_item_by_slot(ITEM_SLOT_OCLOTHING) - if(!istype(S)) - return FALSE - user.visible_message(span_warning("[user] vomits a glob of acid across the front of [user.p_their()] [S]!"), \ - span_warning("We vomit acidic ooze onto our [user.wear_suit.name]!")) - addtimer(CALLBACK(src, PROC_REF(dissolve_straightjacket), user, S), 3 SECONDS) - log_combat(user, user.wear_suit, "melted [user.wear_suit]", addition = "(biodegrade)") - ..() - return TRUE + for(var/obj/restraint as anything in restraints) + if(restraint.obj_flags & (INDESTRUCTIBLE | ACID_PROOF | UNACIDABLE)) + to_chat(user, span_changeling("We cannot use bio-acid to destroy [restraint]!")) + continue - if(istype(user.loc, /obj/structure/closet)) - var/obj/structure/closet/C = user.loc - if(!istype(C)) - return FALSE - C.visible_message(span_warning("[C]'s hinges suddenly begin to melt and run!")) - to_chat(user, span_warning("We vomit acidic goop onto the interior of [C]!")) - addtimer(CALLBACK(src, PROC_REF(open_closet), user, C), 7 SECONDS) - log_combat(user, user.loc, "melted locker", addition = "(biodegrade)") - ..() - return TRUE - - if(istype(user.loc, /obj/structure/spider/cocoon)) - var/obj/structure/spider/cocoon/C = user.loc - if(!istype(C)) - return FALSE - C.visible_message(span_warning("[src] shifts and starts to fall apart!")) - to_chat(user, span_warning("We secrete acidic enzymes from our skin and begin melting our cocoon...")) - addtimer(CALLBACK(src, PROC_REF(dissolve_cocoon), user, C), 25) //Very short because it's just webs - log_combat(user, user.loc, "melted cocoon", addition = "(biodegrade)") - ..() - return TRUE - - var/obj/item/clothing/shoes/shoes = user.shoes - if(istype(shoes) && shoes.tied == SHOES_KNOTTED && !(shoes.resistance_flags & (INDESTRUCTIBLE|UNACIDABLE|ACID_PROOF))) - new /obj/effect/decal/cleanable/greenglow(shoes.drop_location()) + if(restraint == user.loc) + restraint.visible_message(span_warning("Bubbling acid start spewing out of [restraint]...")) + addtimer(CALLBACK(restraint, TYPE_PROC_REF(/atom, atom_destruction), ACID), 4 SECONDS) + for(var/beat in 1 to 3) + addtimer(CALLBACK(src, PROC_REF(make_puddle), restraint), beat SECONDS) + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), restraint, 'sound/items/tools/welder.ogg', 50, TRUE), beat SECONDS) + log_combat(user = user, target = restraint, what_done = "melted restraining container", addition = "(biodegrade)") + return + addtimer(CALLBACK(restraint, TYPE_PROC_REF(/atom, atom_destruction), ACID), 1.5 SECONDS) + log_combat(user = user, target = restraint, what_done = "melted restraining item", addition = "(biodegrade)") user.visible_message( - span_warning("[user] vomits a glob of acid on [user.p_their()] tied up [shoes.name], melting [shoes.p_them()] into a pool of goo!"), - span_warning("We vomit acidic ooze onto our tied up [shoes.name], melting [shoes.p_them()] into a pool of goo!"), - ) - log_combat(user, shoes, "melted own shoes", addition = "(biodegrade)") - qdel(shoes) - ..() - return TRUE + span_warning("[user] spews torrents of acid onto [restraint], melting them with horrifying ease."), + user.balloon_alert(user, "melting restraints..."), + span_danger("You hear retching, then the sizzling of powerful acid, closer to the sound of hissing steam.")) + playsound(user, 'sound/items/tools/welder.ogg', 50, TRUE) + . = TRUE - user.balloon_alert(user, "already free!") - return FALSE + if(space_invader) + punish_with_acid(user, space_invader) + . = TRUE + return . -/datum/action/changeling/biodegrade/proc/dissolve_handcuffs(mob/living/carbon/human/user, obj/O) - if(O && user.handcuffed == O) - user.visible_message(span_warning("[O] dissolve[O.gender == PLURAL?"":"s"] into a puddle of sizzling goop.")) - new /obj/effect/decal/cleanable/greenglow(O.drop_location()) - qdel(O) +/// Spawn green acid puddle underneath obj, used for callback +/datum/action/changeling/biodegrade/proc/make_puddle(obj/melted_restraint) + if (melted_restraint) // just incase obj gets qdel'd + return new /obj/effect/decal/cleanable/greenglow(get_turf(melted_restraint)) -/datum/action/changeling/biodegrade/proc/dissolve_legcuffs(mob/living/carbon/human/user, obj/O) - if(O && user.legcuffed == O) - user.visible_message(span_warning("[O] dissolve[O.gender == PLURAL?"":"s"] into a puddle of sizzling goop.")) - new /obj/effect/decal/cleanable/greenglow(O.drop_location()) - qdel(O) +/datum/action/changeling/biodegrade/proc/acid_blast(atom/movable/user, atom/movable/target) + var/datum/reagents/ephemeral_acid = new + ephemeral_acid.add_reagent(bio_acid_path, bio_acid_amount_per_spray) + var/mutable_appearance/splash_animation = mutable_appearance('icons/effects/effects.dmi', "splash") + splash_animation.color = bio_acid_color + target.flick_overlay_view(splash_animation, 3 SECONDS) + ephemeral_acid.expose(target, TOUCH) -/datum/action/changeling/biodegrade/proc/dissolve_straightjacket(mob/living/carbon/human/user, obj/S) - if(S && user.wear_suit == S) - user.visible_message(span_warning("[S] dissolves into a puddle of sizzling goop.")) - new /obj/effect/decal/cleanable/greenglow(S.drop_location()) - qdel(S) - -/datum/action/changeling/biodegrade/proc/open_closet(mob/living/carbon/human/user, obj/structure/closet/C) - if(C && user.loc == C) - C.visible_message(span_warning("[C]'s door breaks and opens!")) - new /obj/effect/decal/cleanable/greenglow(C.drop_location()) - C.welded = FALSE - C.locked = FALSE - C.broken = TRUE - C.open() - to_chat(user, span_warning("We open the container restraining us!")) - -/datum/action/changeling/biodegrade/proc/dissolve_cocoon(mob/living/carbon/human/user, obj/structure/spider/cocoon/C) - if(C && user.loc == C) - new /obj/effect/decal/cleanable/greenglow(C.drop_location()) - qdel(C) //The cocoon's destroy will move the changeling outside of it without interference - to_chat(user, span_warning("We dissolve the cocoon!")) +/datum/action/changeling/biodegrade/proc/punish_with_acid(mob/living/carbon/human/user, mob/living/hapless_manhandler) + acid_blast(user, hapless_manhandler) + playsound(user, 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg', 50, TRUE) + if(IS_CHANGELING(hapless_manhandler)) + user.visible_message( + span_danger("[user] spews a mist of sizzling acid onto [hapless_manhandler]... but nothing happens!"), + span_changeling("We prepare our escape, spraying bio-acid on our captor... [span_danger("But nothing happened?!")]"), + span_danger("You hear retching, then a sizzling that terminates quite abruptly.") + ) + to_chat(hapless_manhandler, span_changeling("Our prey attempts to dissuade us with one of our biology's simplest adaptions. Quaint.")) + return + user.visible_message( + span_danger("[user] spews a mist of sizzling acid onto [hapless_manhandler], using the opportunity to wrestle away."), + user.balloon_alert(user, "dissuading captor..."), + span_danger("You hear retching, then sizzling, quickly muffled by a loud keening of pain.")) + hapless_manhandler.Stun(2 SECONDS) + hapless_manhandler.emote("scream") + hapless_manhandler.stop_pulling() + log_combat(user = user, target = hapless_manhandler, what_done = "acid-spewed to escape a grab", addition = "(biodegrade)") diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 20790b40ea3..ed28dcb015c 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -439,7 +439,7 @@ ///Get the item on the mob in the storage slot identified by the id passed in -/mob/proc/get_item_by_slot(slot_id) +/mob/proc/get_item_by_slot(slot_id) as /obj/item return null /// Gets what slot the item on the mob is held in. diff --git a/tgstation.dme b/tgstation.dme index acbea823808..36b93c48fe8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3301,6 +3301,7 @@ #include "code\modules\antagonists\changeling\changeling_power.dm" #include "code\modules\antagonists\changeling\fallen_changeling.dm" #include "code\modules\antagonists\changeling\headslug_eggs.dm" +#include "code\modules\antagonists\changeling\powers\_biodegrade_bioacid.dm" #include "code\modules\antagonists\changeling\powers\absorb.dm" #include "code\modules\antagonists\changeling\powers\adrenaline.dm" #include "code\modules\antagonists\changeling\powers\augmented_eyesight.dm"