Changeling's last resort breaks near obstacles (#93109)

## About The Pull Request

- **fixes** a runtime error that occurred when removing antag info
button from the ling's old body (which became null) after transforming
into a worm.
- **fixes** a bug that caused a worm to appear in the debug room when
spamming the "yes" button in the TGUI alert window
- **changes** the blind range of last resort from 2 to 4, because it has
a short animation now

**Added** the ability for the changeling's "Last Resort" to break
through walls and other objects accompanied by blood, gore, and
disgusting sounds😊


https://github.com/user-attachments/assets/a2f263ec-a809-404f-a001-8d3643ba887b
## Why It's Good For The Game
currently, the explosion from "last resort" feels generic, similar to a
grenade or bomb. As a bioweapon, the changeling's ability should reflect
its biological nature — as if the ling is rapidly expanding its cells,
bursting its own body to rupture the surrounding environment and
explode.

regarding the ability to break walls: the current design allows for a
counter-play strategy of quickly building walls around a ling (it takes
like 10 seconds or less) who is in stasis or accumulating reagents for
last resort. While containing a ling in proper cell is valid roleplay
(like for science), this "fortnite-style" walling tactic feels cheap.
The intended design of the ability is to force the destruction of the
ling's body, not to allow them to be permanently walled in with no
option other than ghosting.

Yeah, i know that players will just build a cell that's 2 walls thick or
even bigger, but i think that's fiiine.
## Changelog
🆑
add: changeling's last resort ability now can rupture surrounding them
walls and other objects
add: last resort now has a short animation
balance: increased last resort's blind range from 2 to 4 tiles
fix: fixed several runtimes related to changeling's last resort ability
sound: added a gore sound effect to last resort explosion
/🆑
This commit is contained in:
paganiy
2025-10-11 21:56:34 +00:00
committed by GitHub
parent 89d8d0d3d3
commit 33ea581933
5 changed files with 113 additions and 25 deletions
@@ -191,7 +191,8 @@ GLOBAL_LIST_EMPTY(antagonists)
old_body.remove_from_current_living_antags()
var/datum/action/antag_info/info_button = info_button_ref?.resolve()
if(info_button)
info_button.Remove(old_body)
if(old_body)
info_button.Remove(old_body)
info_button.Grant(new_body)
apply_innate_effects(new_body)
if(new_body.stat != DEAD)
@@ -1,7 +1,10 @@
#define LAST_RESORT_EXPLOSION_RANGE 2
#define LAST_RESORT_BLIND_RANGE 4
/datum/action/changeling/headcrab
name = "Last Resort"
desc = "We sacrifice our current body in a moment of need, placing us in control of a vessel that can plant our likeness in a new host. Costs 20 chemicals."
helptext = "We will be placed in control of a small, fragile creature. We may attack a corpse like this to plant an egg which will slowly mature into a new form for us."
desc = "We sacrifice our current body in a moment of need, violently expanding to break through obstacles and reforming as a headslug. Costs 20 chemicals."
helptext = "We will violently expand, destroying obstacles around us, then be placed in control of a small, fragile creature. We may attack a corpse like this to plant an egg which will slowly mature into a new form for us."
button_icon_state = "last_resort"
chemical_cost = 20
dna_cost = 1
@@ -19,34 +22,66 @@
/datum/action/changeling/headcrab/sting_action(mob/living/user)
set waitfor = FALSE
var/confirm = tgui_alert(user, "Are we sure we wish to destroy our body and create a headslug?", "Last Resort", list("Yes", "No"))
if(confirm != "Yes")
if(active || confirm != "Yes")
return
active = TRUE
..()
user.visible_message(span_boldwarning("[user]'s body begins to pulsate and swell unnaturally!"))
playsound(user, 'sound/effects/wounds/crack1.ogg', 100, TRUE)
animate(user, transform = user.transform * 1.5, color = COLOR_RED, time = 1 SECONDS)
if(is_walled(user))
user.Immobilize(1.5 SECONDS) // to prevent the breaking of the wrong walls (who would think of using the antistun after the last resort)
escaping_prison(user)
addtimer(CALLBACK(src, PROC_REF(gibbing), user), 1.1 SECONDS)
/datum/action/changeling/headcrab/proc/gibbing(mob/living/user)
if(QDELETED(user))
active = FALSE
return
gore_explosion(user)
var/datum/mind/stored_mind = user.mind
var/list/organs = user.get_organs_for_zone(BODY_ZONE_HEAD, TRUE)
explosion(user, light_impact_range = 2, adminlog = TRUE, explosion_cause = src)
for(var/mob/living/carbon/human/blinded_human in range(2, user))
var/obj/item/organ/eyes/eyes = blinded_human.get_organ_slot(ORGAN_SLOT_EYES)
if(!eyes || blinded_human.is_blind())
continue
to_chat(blinded_human, span_userdanger("You are blinded by a shower of blood!"))
blinded_human.Stun(4 SECONDS)
blinded_human.set_eye_blur_if_lower(40 SECONDS)
blinded_human.adjust_confusion(12 SECONDS)
for(var/mob/living/silicon/blinded_silicon in range(2,user))
to_chat(blinded_silicon, span_userdanger("Your sensors are disabled by a shower of blood!"))
blinded_silicon.Paralyze(6 SECONDS)
var/turf/user_turf = get_turf(user)
user.transfer_observers_to(user_turf) // user is about to be deleted, store orbiters on the turf
if(user.stat != DEAD)
user.investigate_log("has been gibbed by headslug burst.", INVESTIGATE_DEATHS)
user.gib(DROP_ALL_REMAINS)
. = TRUE
addtimer(CALLBACK(src, PROC_REF(spawn_headcrab), stored_mind, user_turf, organs), 1 SECONDS)
addtimer(CALLBACK(src, PROC_REF(spawn_headcrab), stored_mind, user_turf, organs), 0.5 SECONDS) // without this delay the worm will be almost dead due to the explosion :(
/// Creates a light explosion, blinds and confuses mobs in range
/datum/action/changeling/headcrab/proc/gore_explosion(mob/living/user)
var/list/user_DNA = user.get_blood_dna_list()
user.visible_message(span_boldwarning("[user]'s body ruptures in a violent explosion of biomass!"))
playsound(user, 'sound/effects/goresplat.ogg', 100, TRUE) //yuck!!
explosion(user, light_impact_range = LAST_RESORT_EXPLOSION_RANGE, flame_range = 0, flash_range = 0, adminlog = TRUE, silent = TRUE, explosion_cause = src)
user.spawn_gibs()
for(var/turf/bloody_turf in view(LAST_RESORT_BLIND_RANGE, user))
var/obj/effect/decal/cleanable/blood/blood_spot = new(bloody_turf)
blood_spot.add_blood_DNA(user_DNA)
for(var/mob/living/blinded in view(LAST_RESORT_BLIND_RANGE, user))
if(blinded == user)
continue
blinded.visible_message(span_danger("[blinded] is splattered with blood!"), span_userdanger("You're splattered with blood!"))
blinded.add_blood_DNA(user_DNA)
playsound(blinded, 'sound/effects/splat.ogg', 50, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
if(ishuman(blinded))
var/mob/living/carbon/human/blinded_human = blinded
var/obj/item/organ/eyes/eyes = blinded_human.get_organ_slot(ORGAN_SLOT_EYES)
if(!eyes || blinded_human.is_blind())
continue
to_chat(blinded_human, span_userdanger("You are blinded by a shower of blood!"))
blinded_human.Stun(4 SECONDS)
blinded_human.set_eye_blur_if_lower(40 SECONDS)
blinded_human.adjust_confusion(12 SECONDS)
if(issilicon(blinded))
var/mob/living/silicon/blinded_silicon = blinded
to_chat(blinded_silicon, span_userdanger("Your sensors are disabled by a shower of blood!"))
blinded_silicon.Paralyze(6 SECONDS)
/// Creates the headrab to occupy
/datum/action/changeling/headcrab/proc/spawn_headcrab(datum/mind/stored_mind, turf/spawn_location, list/organs)
@@ -56,4 +91,53 @@
stored_mind.transfer_to(crab, force_key_move = TRUE)
spawn_location.transfer_observers_to(crab)
to_chat(crab, span_warning("You burst out of the remains of your former body in a shower of gore!"))
to_chat(crab, span_warning("We burst out of the remains of our former body in a shower of gore!"))
active = FALSE
/// Ruptures nearby walls using the torn_wall component. Also it destroys objects with density.
/datum/action/changeling/headcrab/proc/escaping_prison(mob/living/user)
user.visible_message(span_boldwarning("[user]'s expanding form begins crushing the surrounding obstacles!"))
var/list/walls_to_destroy = list()
for(var/turf/nearby_turf in range(1, user))
if(iswallturf(nearby_turf))
var/turf/closed/wall/nearby_wall = nearby_turf
var/datum/component/torn_wall/torn_comp = nearby_wall.GetComponent(/datum/component/torn_wall)
if(!torn_comp)
nearby_wall.AddComponent(/datum/component/torn_wall, current_stage = 1)
else
torn_comp.current_stage = 1
walls_to_destroy += nearby_wall
continue
for(var/obj/obj_obstacle in nearby_turf)
if(!obj_obstacle.density || (obj_obstacle.resistance_flags & INDESTRUCTIBLE) || !obj_obstacle.anchored)
continue
if(obj_obstacle.uses_integrity)
obj_obstacle.take_damage(300)
addtimer(CALLBACK(src, PROC_REF(finalize_destruction), walls_to_destroy, user, user.loc), 1 SECONDS)
/// Completes the destruction of the walls after a 1-second delay for more drama
/datum/action/changeling/headcrab/proc/finalize_destruction(list/affected_walls, mob/living/user, atom/user_prev_loc)
if(QDELETED(user) || (user.loc != user_prev_loc))
active = FALSE
return
for(var/turf/closed/wall/wall in affected_walls)
if(QDELETED(wall))
continue
wall.AddComponent(/datum/component/torn_wall)
/datum/action/changeling/headcrab/proc/is_walled(mob/living/user)
var/turf/ling_turf = get_turf(user)
if(!ling_turf)
return FALSE
var/blocked_directions
for(var/dir in GLOB.cardinals)
var/turf/neighbor = get_step(ling_turf, dir)
if(!neighbor || neighbor.is_blocked_turf(exclude_mobs = TRUE))
blocked_directions++
return blocked_directions >= 3 //1x2 cage won't be able to contain linga
#undef LAST_RESORT_EXPLOSION_RANGE
#undef LAST_RESORT_BLIND_RANGE