Adds a 0 point quirk that turns your body into a recovered crewmember (effectively) on DNRing (#95469)

## About The Pull Request

Adds `Thanatorenasia`, a 0 point neutral quirk

Those with `Thanatorenasia`, upon DNRing, their body will be made into a
"recovered crewmember" - meaning when the body is revived, ghosts will
be polled and allowed to take control over it

The body will retain `Thanatorenasia` after the fact, so the new
controller can DNR as well, continuing the cycle

## Why It's Good For The Game

A random idea I had based on a recent interaction that I thought could
be cool character gimmick. Every time someone dies, they get revived
with a different player, so they act differently, think differently, and
don't remember the rest of the round.

## Changelog

🆑 Melbert
add: Adds Thanatorenasia, a 0 point neutral quirk that turns your body
into a "recovered crewmember" if you choose to DNR - meaning if revived,
any ghost could take control of it
refactor: Refactored "ghostrol on revive" behavior a fair bit, report
any oddities with it / the spawners menu
/🆑
This commit is contained in:
MrMelbert
2026-03-22 19:01:48 -06:00
committed by GitHub
parent a7ab79f34f
commit 6734968a57
12 changed files with 189 additions and 102 deletions
@@ -381,3 +381,6 @@
/// Sent to a mob when one of their bodypart's surgery state changes, OR sent from the basic_surgery_state holder when its surgery state changes (old_state, new_state, changed_states)
#define COMSIG_LIVING_UPDATING_SURGERY_STATE "carbon_updating_surgery_state"
/// Sent to a mob when its player DNRs
#define COMSIG_LIVING_DNR "living_dnr"
+2
View File
@@ -17,3 +17,5 @@
/// Quirk is similar to brain trauma and should be shown in medical guides as one.
/// You don't need to set this on quirks that apply a trauma, that's redundant.
#define QUIRK_TRAUMALIKE (1<<5)
/// Do not transfer this quirk via transfer_quirk_datums
#define QUIRK_NO_TRANSFER (1<<6)
-3
View File
@@ -1597,9 +1597,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Trait that signals to objects on this turf that its open (has UNDERFLOOR_INTERACTIBLE) but still covers them
#define TRAIT_UNCOVERED_TURF "uncovered_turf"
/// A trait that blocks the metabolism of formaldehyde
#define TRAIT_BLOCK_FORMALDEHYDE_METABOLISM "block_formaldehyde_metabolism"
///Attached to objects currently on tables and such, allowing them to walk on other objects without the climbing delay
#define TRAIT_ON_CLIMBABLE "on_climbable"
-1
View File
@@ -195,7 +195,6 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_BIRTHDAY_BOY" = TRAIT_BIRTHDAY_BOY,
"TRAIT_BLOB_ALLY" = TRAIT_BLOB_ALLY,
"TRAIT_BLOCKING_PROJECTILES" = TRAIT_BLOCKING_PROJECTILES,
"TRAIT_BLOCK_FORMALDEHYDE_METABOLISM" = TRAIT_BLOCK_FORMALDEHYDE_METABOLISM,
"TRAIT_BLOCK_SECHUD" = TRAIT_BLOCK_SECHUD,
"TRAIT_BLOCK_SHUTTLE_MOVEMENT" = TRAIT_BLOCK_SHUTTLE_MOVEMENT,
"TRAIT_BLOODSHOT_EYES" = TRAIT_BLOODSHOT_EYES,
+118 -87
View File
@@ -6,14 +6,31 @@
var/datum/callback/on_successful_revive
/// The chance to twitch when orbiting the spawn
var/twitch_chance = 30
/// The title shown to ghosts when polled to enter the body
var/revive_title
/datum/component/ghostrole_on_revive/Initialize(refuse_revival_if_failed, on_successful_revive)
. = ..()
// Text displayed in the spawners menu
var/spawn_text
var/you_are_text
var/flavor_text
var/important_text
/datum/component/ghostrole_on_revive/Initialize(
refuse_revival_if_failed = FALSE,
datum/callback/on_successful_revive,
revive_title = "a recovered crewmember",
spawn_text = "Recovered Crew",
you_are_text = "You are a long dead crewmember, but are soon to be revived to rejoin the crew!",
flavor_text = "Get a job and get back to work!",
important_text = "Do your best to help the station. You still roll for midround antagonists."
)
src.refuse_revival_if_failed = refuse_revival_if_failed
src.on_successful_revive = on_successful_revive
ADD_TRAIT(parent, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) //for adding an alternate examination
src.revive_title = revive_title
src.spawn_text = spawn_text
src.you_are_text = you_are_text
src.flavor_text = flavor_text
src.important_text = important_text
if(ismob(parent))
prepare_mob(parent)
@@ -22,138 +39,152 @@
if(!istype(parent, /obj/item/organ/brain))
return COMPONENT_INCOMPATIBLE
var/obj/item/organ/brain/brein = parent
if(brein.owner)
prepare_mob(brein.owner)
var/obj/item/organ/brain/brain_parent = parent
if(brain_parent.owner)
prepare_mob(brain_parent.owner)
else
prepare_brain(brein)
prepare_brain(brain_parent)
/// Give the appropriate signals, and watch for organ removal
/datum/component/ghostrole_on_revive/proc/prepare_mob(mob/living/liver)
RegisterSignal(liver, COMSIG_LIVING_REVIVE, PROC_REF(on_revive))
ADD_TRAIT(liver, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
/datum/component/ghostrole_on_revive/proc/prepare_mob(mob/living/to_prepare)
RegisterSignal(to_prepare, COMSIG_LIVING_REVIVE, PROC_REF(on_revive))
RegisterSignal(to_prepare, COMSIG_MOB_REAGENT_TICK, PROC_REF(block_formaldehyde_metabolism))
if(istype(parent, /obj/item/organ/brain))
RegisterSignal(to_prepare, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_remove))
ADD_TRAIT(to_prepare, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
add_orbit_twitching(liver)
add_orbit_twitching(to_prepare)
to_prepare.med_hud_set_status()
liver.med_hud_set_status()
/datum/component/ghostrole_on_revive/proc/unprepare_mob(mob/living/to_unprepare)
REMOVE_TRAIT(to_unprepare, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
UnregisterSignal(to_unprepare, list(
COMSIG_LIVING_REVIVE,
COMSIG_MOB_REAGENT_TICK,
COMSIG_CARBON_LOSE_ORGAN,
))
if(iscarbon(liver))
var/mob/living/carbon/carbon = liver
var/obj/item/organ/brain = carbon.get_organ_by_type(/obj/item/organ/brain)
if(brain)
RegisterSignal(brain, COMSIG_ORGAN_REMOVED, PROC_REF(on_remove))
to_unprepare.med_hud_set_status()
remove_orbit_twitching(to_unprepare)
/datum/component/ghostrole_on_revive/proc/on_remove(obj/item/organ/brain, mob/living/old_owner)
/datum/component/ghostrole_on_revive/proc/on_remove(mob/living/carbon/source, obj/item/organ/removed_brain)
SIGNAL_HANDLER
REMOVE_TRAIT(old_owner, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
remove_orbit_twitching(old_owner)
if(!istype(removed_brain, /obj/item/organ/brain))
return
unprepare_mob(source)
// we might have some lingering blinking eyes
var/obj/item/bodypart/head/head = old_owner?.get_bodypart(BODY_ZONE_HEAD)
var/obj/item/bodypart/head/head = source.get_bodypart(BODY_ZONE_HEAD)
if(head)
var/soul_eyes = locate(/datum/bodypart_overlay/simple/soul_pending_eyes) in head.bodypart_overlays
if(soul_eyes)
head.remove_bodypart_overlay(soul_eyes)
prepare_brain(brain)
prepare_brain(removed_brain)
/datum/component/ghostrole_on_revive/proc/prepare_brain(obj/item/organ/brein)
/datum/component/ghostrole_on_revive/proc/prepare_brain(obj/item/organ/source)
ADD_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
RegisterSignal(source, COMSIG_ORGAN_IMPLANTED, PROC_REF(prepare_mob_from_brain))
UnregisterSignal(source, COMSIG_ORGAN_REMOVED)
/datum/component/ghostrole_on_revive/proc/unprepare_brain(obj/item/organ/source)
REMOVE_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED)
RegisterSignal(source, COMSIG_ORGAN_REMOVED, PROC_REF(prepare_brain))
source.owner?.med_hud_set_status()
/datum/component/ghostrole_on_revive/proc/prepare_mob_from_brain(obj/item/organ/brain/source, mob/living/owner)
SIGNAL_HANDLER
RegisterSignal(brein, COMSIG_ORGAN_IMPLANTED, PROC_REF(prepare_mob_from_brain))
UnregisterSignal(brein, COMSIG_ORGAN_REMOVED)
/datum/component/ghostrole_on_revive/proc/prepare_mob_from_brain(obj/item/organ/brain/brein, mob/living/owner)
SIGNAL_HANDLER
UnregisterSignal(brein, COMSIG_ORGAN_IMPLANTED)
REMOVE_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED)
prepare_mob(owner)
/datum/component/ghostrole_on_revive/proc/on_revive(mob/living/aliver)
/datum/component/ghostrole_on_revive/proc/on_revive(mob/living/source)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(poll_ghosts), aliver)
INVOKE_ASYNC(src, PROC_REF(poll_ghosts), source)
/datum/component/ghostrole_on_revive/proc/poll_ghosts(mob/living/aliver)
var/soul_eyes
var/obj/item/bodypart/head
/datum/component/ghostrole_on_revive/proc/poll_ghosts(mob/living/reviving)
var/datum/bodypart_overlay/simple/soul_pending_eyes/soul_eyes
// adds soulful SOUL PENDING eyes to indicate what's happening to observers
var/obj/item/bodypart/head/head = reviving.get_bodypart(BODY_ZONE_HEAD)
if(head)
soul_eyes = new()
head.add_bodypart_overlay(soul_eyes)
var/mob/living/carbon/human/hewmon
if(ishuman(aliver))
hewmon = aliver
head = hewmon.get_bodypart(BODY_ZONE_HEAD)
if(head)
soul_eyes = new /datum/bodypart_overlay/simple/soul_pending_eyes()
head.add_bodypart_overlay(soul_eyes)
// So during the potentially short period of time we're revived, we don't metabolize formaldehyde
// Really just a QOL for coroners, so we dont suddenly start decaying
ADD_TRAIT(aliver, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM, type)
var/list/jobbans = list(ROLE_RECOVERED_CREW)
if(reviving.mind)
jobbans |= reviving.mind.assigned_role.title
for(var/datum/antagonist/antag as anything in reviving.mind.antag_datums)
if(antag.jobban_flag)
jobbans |= antag.jobban_flag
if(antag.pref_flag)
jobbans |= antag.pref_flag
if(!(antag.antag_flags & ANTAG_FAKE))
jobbans |= ROLE_SYNDICATE
var/mob/dead/observer/chosen_one = SSpolling.poll_ghosts_for_target(
question = "Would you like to play as a recovered crewmember?",
role = null,
check_jobban = ROLE_RECOVERED_CREW,
question = "Would you like to play as [revive_title]?",
check_jobban = jobbans,
poll_time = 15 SECONDS,
checked_target = aliver,
checked_target = reviving,
ignore_category = POLL_IGNORE_RECOVERED_CREW,
alert_pic = aliver,
role_name_text = "recovered crew",
alert_pic = reviving,
role_name_text = revive_title,
)
REMOVE_TRAIT(aliver, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM, type)
if(head)
head.remove_bodypart_overlay(soul_eyes)
if(soul_eyes)
qdel(soul_eyes)
if(!isobserver(chosen_one))
if(refuse_revival_if_failed)
aliver.death()
aliver.visible_message(span_deadsay("[aliver.name]'s soul is struggling to return!"))
else
aliver.PossessByPlayer(chosen_one.ckey)
on_successful_revive?.Invoke(aliver)
if(isobserver(chosen_one))
reviving.PossessByPlayer(chosen_one.ckey)
on_successful_revive?.Invoke(reviving)
qdel(src)
/datum/component/ghostrole_on_revive/proc/add_orbit_twitching(mob/living/liver)
liver.AddElement(/datum/element/orbit_twitcher, twitch_chance)
else if(refuse_revival_if_failed)
reviving.death()
reviving.visible_message(span_deadsay("[reviving]'s soul is struggling to return!"))
/datum/component/ghostrole_on_revive/proc/add_orbit_twitching(mob/living/parent_mob)
parent_mob.AddElement(/datum/element/orbit_twitcher, twitch_chance)
// Add it to the ghostrole spawner menu. Note that we can't directly spawn from it, but we can make it twitch to alert bystanders to defib it
LAZYADD(GLOB.joinable_mobs[format_text("Recovered Crew")], liver)
RegisterSignal(liver, COMSIG_LIVING_GHOSTROLE_INFO, PROC_REF(set_spawner_info))
LAZYADDASSOCLIST(GLOB.joinable_mobs, spawn_text, parent_mob)
RegisterSignal(parent_mob, COMSIG_LIVING_GHOSTROLE_INFO, PROC_REF(set_spawner_info))
/datum/component/ghostrole_on_revive/proc/set_spawner_info(datum/spawners_menu/menu, string_info)
/datum/component/ghostrole_on_revive/proc/set_spawner_info(datum/spawners_menu/menu, list/string_info)
SIGNAL_HANDLER
string_info["you_are_text"] = "You are a long dead crewmember, but are soon to be revived to rejoin the crew!"
string_info["flavor_text"] = "Get a job and get back to work!"
string_info["important_text"] = "Do your best to help the station. You still roll for midround antagonists."
string_info["you_are_text"] = src.you_are_text
string_info["flavor_text"] = src.flavor_text
string_info["important_text"] = src.important_text
/datum/component/ghostrole_on_revive/proc/remove_orbit_twitching(mob/living/living)
living.RemoveElement(/datum/element/orbit_twitcher, twitch_chance)
/datum/component/ghostrole_on_revive/proc/remove_orbit_twitching(mob/living/parent_mob)
parent_mob.RemoveElement(/datum/element/orbit_twitcher, twitch_chance)
LAZYREMOVEASSOC(GLOB.joinable_mobs, spawn_text, parent_mob)
UnregisterSignal(parent_mob, COMSIG_LIVING_GHOSTROLE_INFO)
// Remove from the ghostrole spawning menu
var/list/spawners = GLOB.joinable_mobs[format_text("Recovered Crew")]
LAZYREMOVE(spawners, living)
// Block formaldehyde from being metabolized, Coroner QoL
/datum/component/ghostrole_on_revive/proc/block_formaldehyde_metabolism(mob/living/source, datum/reagent/chem)
SIGNAL_HANDLER
if(!LAZYLEN(spawners))
GLOB.joinable_mobs -= format_text("Recovered Crew")
UnregisterSignal(living, COMSIG_LIVING_GHOSTROLE_INFO)
if(istype(chem, /datum/reagent/toxin/formaldehyde))
return COMSIG_MOB_STOP_REAGENT_TICK
/datum/component/ghostrole_on_revive/Destroy(force)
REMOVE_TRAIT(parent, TRAIT_GHOSTROLE_ON_REVIVE, REF(src))
var/mob/living/living
if(isliving(parent))
living = parent
unprepare_mob(parent)
else if(istype(parent, /obj/item/organ/brain))
var/obj/item/organ/brain/brain = parent
living = brain.owner
living?.med_hud_set_status()
if(living)
remove_orbit_twitching(living)
if(brain.owner)
unprepare_mob(brain.owner)
else
unprepare_brain(brain)
return ..()
+2
View File
@@ -286,5 +286,7 @@
var/datum/preferences/to_pass = client || to_mob.client
for(var/datum/quirk/quirk as anything in quirks)
if(quirk.quirk_flags & QUIRK_NO_TRANSFER)
continue
quirk.remove_from_current_holder(quirk_transfer = TRUE)
quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass)
@@ -0,0 +1,55 @@
/datum/quirk/death_dnr_poll
name = "Thanatorenasia"
desc = "Whenever you die and elect to \"Do Not Resuscitate\", your body may be taken over by another ghost upon revival - \
giving it an entirely new personality and fresh set of memories."
icon = FA_ICON_ZAP
value = 0
medical_record_text = "Patient has Thanatorenasia - in the event of their death and resuscitation, \
they may experience memory loss or a change in personality."
medical_symptom_text = "In the event of the patient's death and resuscitation, \
they may experience memory loss or a change in personality."
quirk_flags = QUIRK_NO_TRANSFER
// Used the the spawners menu to describe the quirk
var/you_are_text = "You are a deceased crewmember, afflicted with Thanatorenasia - \
a condition which alters personality and causes memory loss upon death and revival."
var/flavor_text = "Something feels... different. \
You're not entirely sure who you are or what happened - All you remember is your name and that you work here. \
Oh well, better get back to work - the last thing you want is to be both unemployed AND an amnesiac."
var/important_text = "Resume your assigned duty. \
If you choose to \"Do Not Resuscitate\" upon death, another ghost will be allowed to take over the body. \
You still roll for midround antagonists."
/datum/quirk/death_dnr_poll/add_unique(client/client_source)
. = ..()
RegisterSignal(quirk_holder, COMSIG_LIVING_DNR, PROC_REF(mob_died))
/datum/quirk/death_dnr_poll/remove()
. = ..()
UnregisterSignal(quirk_holder, COMSIG_LIVING_DNR)
/datum/quirk/death_dnr_poll/proc/mob_died(mob/living/source, mob/dead/observer/dnring)
SIGNAL_HANDLER
var/whomst = source.real_name
if(source.mind && !is_unassigned_job(source.mind.assigned_role))
whomst += "Job: [span_notice(source.mind.assigned_role.title)]."
if(length(source.mind?.get_special_roles()))
whomst += "Status: [span_boldnotice(english_list(source.mind?.get_special_roles()))]."
source.AddComponent(/datum/component/ghostrole_on_revive, \
refuse_revival_if_failed = TRUE, \
on_successful_revive = CALLBACK(src, PROC_REF(on_successful_revive)), \
revive_title = whomst, \
spawn_text = "Deceased Crew", \
you_are_text = src.you_are_text, \
flavor_text = src.flavor_text, \
important_text = src.important_text, \
)
source.log_message("was made ghostrole pollable by [name] quirk.", LOG_GAME, color = COLOR_PURPLE)
/datum/quirk/death_dnr_poll/proc/on_successful_revive()
quirk_holder.log_message("has had their body taken over by a ghost due to the [name] quirk.", LOG_GAME, color = COLOR_PURPLE)
var/welcome_msg = boxed_message(span_notice("<b>[quirk_holder.real_name]</b> has <i>[name]</i> - you are [quirk_holder.p_their()] new owner.<br>\
If you choose to <b>\"Do Not Resuscitate\"</b> upon death, another ghost will take over the body once again."))
addtimer(CALLBACK(src, GLOBAL_PROC_REF(to_chat), quirk_holder, welcome_msg), 2 SECONDS)
+3 -2
View File
@@ -50,9 +50,10 @@
this["name"] = mob_type
this["amount_left"] = 0
for(var/mob/joinable_mob as anything in GLOB.joinable_mobs[mob_type])
SEND_SIGNAL(joinable_mob, COMSIG_LIVING_GHOSTROLE_INFO, this)
this["amount_left"] += 1
if(!SEND_SIGNAL(joinable_mob, COMSIG_LIVING_GHOSTROLE_INFO, this))
this["desc"] = initial(joinable_mob.desc)
this["desc"] ||= initial(joinable_mob.desc)
if(this["amount_left"] > 0)
data["spawners"] += list(this)
return data
@@ -410,6 +410,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
// Update med huds
current_mob.med_hud_set_status()
current_mob.log_message("had their player ([key_name(src)]) do-not-resuscitate / DNR", LOG_GAME, color = COLOR_GREEN, log_globally = FALSE)
SEND_SIGNAL(current_mob, COMSIG_LIVING_DNR, src)
log_message("has opted to do-not-resuscitate / DNR from their body ([current_mob])", LOG_GAME, color = COLOR_GREEN)
// Disassociates observer mind from the body mind
+2 -2
View File
@@ -237,7 +237,7 @@
var/obj/item/organ/brain/brain = get_organ_by_type(/obj/item/organ/brain)
if(brain && isnull(ai_controller))
var/npc_message = ""
if(HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE))
if(HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE) || HAS_TRAIT(src, TRAIT_GHOSTROLE_ON_REVIVE))
npc_message = "Soul is pending..."
else if(!key)
npc_message = "[t_He] [t_is] totally catatonic. The stresses of life in deep-space must have been too much for [t_him]. Any recovery is unlikely."
@@ -324,7 +324,7 @@
var/t_is = p_are()
//This checks to see if the body is revivable
var/obj/item/organ/brain = get_organ_by_type(/obj/item/organ/brain)
if(brain && HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE))
if((brain && HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE)) || HAS_TRAIT(src, TRAIT_GHOSTROLE_ON_REVIVE))
return span_deadsay("[t_He] [t_is] limp and unresponsive; but [t_his] soul might yet come back...")
var/client_like = client || HAS_TRAIT(src, TRAIT_MIND_TEMPORARILY_GONE)
var/valid_ghost = ghost?.can_reenter_corpse && ghost?.client
@@ -785,17 +785,11 @@
if(affected_mob.adjust_tox_loss(-1 * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)) //it counteracts its own toxin damage.
return UPDATE_MOB_HEALTH
return
else if(SPT_PROB(2.5, seconds_per_tick) && !HAS_TRAIT(affected_mob, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM))
else if(SPT_PROB(2.5, seconds_per_tick))
holder.add_reagent(/datum/reagent/toxin/histamine, pick(5,15))
holder.remove_reagent(/datum/reagent/toxin/formaldehyde, 2.4 * metabolization_ratio * seconds_per_tick)
return ..()
/datum/reagent/toxin/formaldehyde/metabolize_reagent(mob/living/carbon/affected_mob, seconds_per_tick, metabolized_volume)
if(HAS_TRAIT(affected_mob, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM))
return
return ..()
/datum/reagent/toxin/venom
name = "Venom"
description = "An exotic poison extracted from highly toxic fauna. Causes scaling amounts of toxin damage and bruising depending and dosage. Often decays into Histamine."
+1
View File
@@ -1953,6 +1953,7 @@
#include "code\datums\quirks\neutral_quirks\pineapple_hater.dm"
#include "code\datums\quirks\neutral_quirks\pineapple_liker.dm"
#include "code\datums\quirks\neutral_quirks\shifty_eyes.dm"
#include "code\datums\quirks\neutral_quirks\thanatorenasia.dm"
#include "code\datums\quirks\neutral_quirks\transhumanist.dm"
#include "code\datums\quirks\neutral_quirks\vegetarian.dm"
#include "code\datums\quirks\positive_quirks\alcohol_tolerance.dm"