diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 71e7b565327..59c83ff1397 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -712,6 +712,9 @@
#define COMSIG_LIVING_MOB_BUMP "living_mob_bump"
///From mob/living/carbon/human/do_suicide()
#define COMSIG_HUMAN_SUICIDE_ACT "human_suicide_act"
+/// Sent from mob/living/carbon/human/do_cpr(): (mob/living/carbon/human/H, new_seconds_of_life)
+#define COMSIG_HUMAN_RECEIVE_CPR "human_receieve_cpr"
+
///From mob/living/carbon/human/attackedby(): (mob/living/carbon/human/attacker). Also found on species/disarm and species/harm
#define COMSIG_HUMAN_ATTACKED "human_attacked"
diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm
index b380d059cd8..5f7532344e2 100644
--- a/code/__DEFINES/misc_defines.dm
+++ b/code/__DEFINES/misc_defines.dm
@@ -402,7 +402,7 @@
// Defib stats
/// Past this much time the patient is unrecoverable (in deciseconds).
-#define DEFIB_TIME_LIMIT (300 SECONDS)
+#define BASE_DEFIB_TIME_LIMIT (300 SECONDS)
/// Brain damage starts setting in on the patient after some time left rotting.
#define DEFIB_TIME_LOSS (60 SECONDS)
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 9009cf90efb..dfe30135dd6 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -173,6 +173,8 @@
#define STATUS_EFFECT_FAKE_VIRUS /datum/status_effect/fake_virus
/// This status effect lets the user see the lwap dots.
#define STATUS_EFFECT_LWAPSCOPE /datum/status_effect/lwap_scope
+/// This status effect allows a mob to be revived with a defibrillator.
+#define STATUS_EFFECT_REVIVABLE /datum/status_effect/limited_bonus/revivable
//////////////////////////
// Mind batter variants //
diff --git a/code/datums/components/defibrillator.dm b/code/datums/components/defibrillator.dm
index 0135702853c..180308aa05d 100644
--- a/code/datums/components/defibrillator.dm
+++ b/code/datums/components/defibrillator.dm
@@ -243,7 +243,7 @@
// Run through some quick failure states after shocking.
var/time_dead = world.time - target.timeofdeath
- if(time_dead > DEFIB_TIME_LIMIT)
+ if(!target.is_revivable())
user.visible_message("[defib_ref] buzzes: Resuscitation failed - Heart tissue damage beyond point of no return for defibrillation.")
defib_success = FALSE
else if(target.getBruteLoss() >= 180 || target.getFireLoss() >= 180)
@@ -280,7 +280,7 @@
target.adjustBruteLoss(-heal_amount)
// Inflict some brain damage scaling with time spent dead
- var/defib_time_brain_damage = min(100 * time_dead / DEFIB_TIME_LIMIT, 99) // 20 from 1 minute onward, +20 per minute up to 99
+ var/defib_time_brain_damage = min(100 * time_dead / BASE_DEFIB_TIME_LIMIT, 99) // 20 from 1 minute onward, +20 per minute up to 99
if(time_dead > DEFIB_TIME_LOSS && defib_time_brain_damage > target.getBrainLoss())
target.setBrainLoss(defib_time_brain_damage)
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 37e7b52c53e..399f072cd65 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -143,6 +143,56 @@
return pick(missed_messages)
+/// A status effect that can have a certain amount of "bonus" duration added, which extends the duration every tick,
+/// although there is a maximum amount of bonus time that can be active at any given time.
+/datum/status_effect/limited_bonus
+ /// How much extra time has been added
+ var/bonus_time = 0
+ /// How much extra time to apply per tick
+ var/bonus_time_per_tick = 1 SECONDS
+ /// How much maximum bonus time can be active at once
+ var/max_bonus_time = 1 MINUTES
+
+/datum/status_effect/limited_bonus/tick()
+ . = ..()
+ // Sure, we could do some fancy stuff with clamping, and it'd probably be a little cleaner.
+ // This keeps the math simple and easier to use later
+ if(bonus_time > bonus_time_per_tick)
+ duration += bonus_time_per_tick
+ bonus_time -= bonus_time_per_tick
+
+/datum/status_effect/limited_bonus/proc/extend(extra_time)
+ bonus_time = clamp(bonus_time + extra_time, 0, max_bonus_time)
+
+/datum/status_effect/limited_bonus/revivable
+ id = "revivable"
+ alert_type = null
+ status_type = STATUS_EFFECT_UNIQUE
+ duration = BASE_DEFIB_TIME_LIMIT
+
+/datum/status_effect/limited_bonus/revivable/on_apply()
+ . = ..()
+ if(!iscarbon(owner))
+ return FALSE
+
+ RegisterSignal(owner, COMSIG_HUMAN_RECEIVE_CPR, PROC_REF(on_cpr))
+ RegisterSignal(owner, COMSIG_LIVING_REVIVE, PROC_REF(on_revive))
+
+
+/datum/status_effect/limited_bonus/revivable/proc/on_cpr(mob/living/carbon/human/H, new_seconds)
+ SIGNAL_HANDLER // COMSIG_HUMAN_RECEIVE_CPR
+ extend(new_seconds)
+
+/datum/status_effect/limited_bonus/revivable/proc/on_revive()
+ SIGNAL_HANDLER // COMSIG_LIVING_REVIVE
+ qdel(src)
+
+/datum/status_effect/limited_bonus/revivable/on_remove()
+ // Update HUDs once the status effect is deleted to show non-revivability
+ INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob/living, med_hud_set_status))
+ . = ..()
+
+
/datum/status_effect/charging
id = "charging"
alert_type = null
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index babc0373312..721d6b50252 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -191,7 +191,7 @@
revivable = FALSE
else if(ismachineperson(src))
revivable = TRUE
- else if(timeofdeath && (round(world.time - timeofdeath) < DEFIB_TIME_LIMIT))
+ else if(timeofdeath && is_revivable())
revivable = TRUE
if(revivable)
diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm
index ac26474b09a..a25f84f7421 100644
--- a/code/game/objects/items/devices/scanners.dm
+++ b/code/game/objects/items/devices/scanners.dm
@@ -160,7 +160,7 @@ REAGENT SCANNER
if(H.timeofdeath && (H.stat == DEAD || (HAS_TRAIT(H, TRAIT_FAKEDEATH))))
to_chat(user, "Time of Death: [station_time_timestamp("hh:mm:ss", H.timeofdeath)]")
var/tdelta = round(world.time - H.timeofdeath)
- if(tdelta < DEFIB_TIME_LIMIT && !DNR)
+ if(H.is_revivable() && !DNR)
to_chat(user, "Subject died [DisplayTimeText(tdelta)] ago, defibrillation may be possible!")
else
to_chat(user, "Subject died [DisplayTimeText(tdelta)] ago.")
diff --git a/code/modules/mob/dead/observer/observer_base.dm b/code/modules/mob/dead/observer/observer_base.dm
index 7e63d61fb81..32e63537c48 100644
--- a/code/modules/mob/dead/observer/observer_base.dm
+++ b/code/modules/mob/dead/observer/observer_base.dm
@@ -405,7 +405,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
create_log(MISC_LOG, "DNR Enabled")
can_reenter_corpse = FALSE
if(!QDELETED(mind.current)) // Could change while they're choosing
- mind.current.med_hud_set_status()
+ mind.current.remove_status_effect(STATUS_EFFECT_REVIVABLE)
/mob/dead/observer/proc/dead_tele()
set category = "Ghost"
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 3178b7d1c3a..cdd553d21b1 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -213,11 +213,6 @@
swap_hand()
/mob/living/carbon/proc/help_shake_act(mob/living/carbon/M)
- if(stat == DEAD)
- if(M != src)
- M.visible_message("[M] desperately shakes [src] trying to wake [p_them()] up, but sadly there is no reaction!", \
- "You shake [src] trying to wake [p_them()], sadly they appear to be too far gone!")
- return
if(health < HEALTH_THRESHOLD_CRIT)
return
if(src == M && ishuman(src))
diff --git a/code/modules/mob/living/carbon/carbon_death.dm b/code/modules/mob/living/carbon/carbon_death.dm
index 8a06d1474a0..037e5aa1b8c 100644
--- a/code/modules/mob/living/carbon/carbon_death.dm
+++ b/code/modules/mob/living/carbon/carbon_death.dm
@@ -4,6 +4,9 @@
if(!.)
return FALSE
+ if(!gibbed)
+ apply_status_effect(STATUS_EFFECT_REVIVABLE)
+
if(reagents)
reagents.death_metabolize(src)
diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm
index 3c923976d1c..d847b838bd4 100644
--- a/code/modules/mob/living/carbon/human/human_mob.dm
+++ b/code/modules/mob/living/carbon/human/human_mob.dm
@@ -1681,49 +1681,152 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
return
..()
+
#define CPR_CHEST_COMPRESSION_ONLY 0.75
#define CPR_RESCUE_BREATHS 1
+#define CPR_CHEST_COMPRESSION_RESTORATION (2 SECONDS)
+#define CPR_BREATHS_RESTORATION (4 SECONDS)
/mob/living/carbon/human/proc/do_cpr(mob/living/carbon/human/H)
+
+ var/static/list/effective_cpr_messages = list(
+ "You feel like you're able to stave off the inevitable for a little longer.",
+ "You can still see the color in their cheeks."
+ )
+
+ var/static/list/ineffective_cpr_messages = list(
+ "You're starting to feel them stiffen under you, but you keep going.",
+ "Without rescue breaths, they seem to be turning a little blue, but you press on.",
+ )
+
if(H == src)
to_chat(src, "You cannot perform CPR on yourself!")
return
- if(H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH))
- to_chat(src, "[H.name] is dead!")
- return
if(H.receiving_cpr) // To prevent spam stacking
to_chat(src, "They are already receiving CPR!")
return
+ if(!can_use_hands() || !has_both_hands())
+ to_chat(src, "You need two hands available to do CPR!")
+ return
+ if(l_hand || r_hand)
+ to_chat(src, "You can't perform effective CPR with your hands full!")
+ return
+
H.receiving_cpr = TRUE
var/cpr_modifier = get_cpr_mod(H)
- visible_message("[src] is trying to perform CPR on [H.name]!", "You try to perform CPR on [H.name]!")
-
- if(do_mob(src, H, 4 SECONDS))
- if(H.health <= HEALTH_THRESHOLD_CRIT)
- H.adjustOxyLoss(-15 * cpr_modifier)
- H.SetLoseBreath(0)
- H.AdjustParalysis(-2 SECONDS)
- H.updatehealth("cpr")
- visible_message("[src] performs CPR on [H.name]!", "You perform CPR on [H.name].")
-
- if(cpr_modifier == CPR_RESCUE_BREATHS)
- to_chat(H, "You feel a breath of fresh air enter your lungs. It feels good.")
+ if(H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH))
+ if(ismachineperson(H) && do_mob(src, H, 4 SECONDS)) // hehe
+ visible_message(
+ "[src] bangs [p_their()] head on [H]'s chassis by accident!",
+ "You go in for a rescue breath, and bang your head on [H]'s machine chassis. CPR's not going to work."
+ )
+ playsound(H, 'sound/weapons/ringslam.ogg', 50, TRUE)
+ adjustBruteLossByPart(2, "head")
H.receiving_cpr = FALSE
- add_attack_logs(src, H, "CPRed", ATKLOG_ALL)
- return TRUE
- else
+ return
+
+ if(HAS_TRAIT(H, TRAIT_FAKEDEATH) || !H.is_revivable())
+ to_chat(src, "[H] is already too far gone for CPR...")
+ H.receiving_cpr = FALSE
+ return
+
+ visible_message("[src] is trying to perform CPR on [H]'s lifeless body!", "You start trying to perform CPR on [H]'s lifeless body!")
+ while(do_mob(src, H, 4 SECONDS) && (H.stat == DEAD) && H.is_revivable())
+ var/timer_restored
+ if(cpr_modifier == CPR_CHEST_COMPRESSION_ONLY)
+ visible_message("[src] gives [H] chest compressions.", "You can't make rescue breaths work, so you do your best to give chest compressions.")
+ timer_restored = CPR_CHEST_COMPRESSION_RESTORATION // without rescue breaths, it won't stave off the death timer forever
+ else
+ visible_message("[src] gives [H] chest compressions and rescue breaths.", "You give [H] chest compressions and rescue breaths.")
+ timer_restored = CPR_BREATHS_RESTORATION // this, however, should keep it indefinitely postponed assuming CPR continues
+
+ if(HAS_TRAIT(H, TRAIT_FAKEDEATH))
+ if(prob(25))
+ to_chat(H, "Your chest burns as you receive unnecessary CPR!")
+ continue
+
+ // this is the amount of time that should get added onto the timer.
+ var/new_time = (timer_restored + H.get_cpr_timer_adjustment(cpr_modifier))
+
+ SEND_SIGNAL(H, COMSIG_HUMAN_RECEIVE_CPR, (new_time SECONDS))
+
+ if(prob(5))
+ if(timer_restored > 4 SECONDS)
+ to_chat(pick(effective_cpr_messages))
+ else
+ to_chat(pick(ineffective_cpr_messages))
+
+ cpr_try_activate_bomb(H)
+
+
+ if(!H.is_revivable())
+ to_chat(src, "You feel [H]'s body is already starting to stiffen beneath you...it's too late for CPR now.")
+ else
+ visible_message("[src] stops giving [H] CPR.", "You stop giving [H] CPR.")
+
H.receiving_cpr = FALSE
- to_chat(src, "You need to stay still while performing CPR!")
+ return
+
+ visible_message("[src] is trying to perform CPR on [H.name]!", "You try to perform CPR on [H.name]!")
+ if(cpr_modifier == CPR_CHEST_COMPRESSION_ONLY)
+ to_chat(src, "You can't get to [H]'s mouth, so your CPR will be less effective!")
+ while(do_mob(src, H, 4 SECONDS) && H.health <= HEALTH_THRESHOLD_CRIT)
+ H.adjustOxyLoss(-15 * cpr_modifier)
+ H.SetLoseBreath(0)
+ H.AdjustParalysis(-2 SECONDS)
+ H.updatehealth("cpr")
+ visible_message("[src] performs CPR on [H.name]!", "You perform CPR on [H.name].")
+
+ cpr_try_activate_bomb(H)
+
+ if(cpr_modifier == CPR_RESCUE_BREATHS)
+ to_chat(H, "You feel a breath of fresh air enter your lungs. It feels good.")
+ add_attack_logs(src, H, "CPRed", ATKLOG_ALL)
+
+ H.receiving_cpr = FALSE
+ visible_message("[src] stops performing CPR on [H].", "You stop performing CPR on [H].")
+ to_chat(src, "You need to stay still while performing CPR!")
/mob/living/carbon/human/proc/get_cpr_mod(mob/living/carbon/human/H)
if(is_mouth_covered() || H.is_mouth_covered())
return CPR_CHEST_COMPRESSION_ONLY
if(!H.check_has_mouth() || !check_has_mouth())
return CPR_CHEST_COMPRESSION_ONLY
+ if(HAS_TRAIT(src, TRAIT_NOBREATH) || HAS_TRAIT(H, TRAIT_NOBREATH))
+ return CPR_CHEST_COMPRESSION_ONLY
+
+ if(dna?.species.breathid != H.dna?.species.breathid) // sorry non oxy-breathers
+ to_chat(src, "You don't think you'd be able to give [H] very effective rescue breaths...")
+ return CPR_CHEST_COMPRESSION_ONLY
return CPR_RESCUE_BREATHS
+/// Get the amount of time that this round of CPR should add to the death timer
+/mob/living/carbon/human/proc/get_cpr_timer_adjustment(cpr_mod)
+ var/time_since_original_death = round(world.time - timeofdeath) / 10
+ // this looks good on desmos I guess
+ // goal here is that you'll get some surplus time added regardless of method if you catch them early
+ var/adjustment_time = max(round(-60 * log((12 + time_since_original_death) / 240)), 0)
+ if(cpr_mod == CPR_CHEST_COMPRESSION_ONLY)
+ // you just won't get as much if you're late
+ adjustment_time /= 2
+
+ return adjustment_time
+
+/mob/living/carbon/human/proc/cpr_try_activate_bomb(mob/living/carbon/human/target)
+ var/obj/item/organ/external/chest/org = target.get_organ("chest")
+ if(istype(org) && istype(org.hidden, /obj/item/grenade))
+ var/obj/item/grenade/G = org.hidden
+ if(!G.active && prob(25))
+ to_chat(src, "You feel something click under your hands.")
+ add_attack_logs(src, target, "activated an implanted grenade [G] in [target] with CPR.", ATKLOG_MOST)
+ playsound(target.loc, 'sound/weapons/armbomb.ogg', 60, TRUE)
+ G.active = TRUE
+ addtimer(CALLBACK(G, TYPE_PROC_REF(/obj/item/grenade, prime)), G.det_time)
+
#undef CPR_CHEST_COMPRESSION_ONLY
#undef CPR_RESCUE_BREATHS
+#undef CPR_CHEST_COMPRESSION_RESTORATION
+#undef CPR_BREATHS_RESTORATION
/mob/living/carbon/human/canBeHandcuffed()
if(get_num_arms() >= 2)
diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm
index e301ae1ad82..717ec33882c 100644
--- a/code/modules/mob/living/carbon/human/species/_species.dm
+++ b/code/modules/mob/living/carbon/human/species/_species.dm
@@ -476,7 +476,7 @@
return TRUE
if(target.on_fire)
user.pat_out(target)
- else if(target.health >= HEALTH_THRESHOLD_CRIT && !HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ else if(target.health >= HEALTH_THRESHOLD_CRIT && !HAS_TRAIT(target, TRAIT_FAKEDEATH) && target.stat != DEAD)
target.help_shake_act(user)
return TRUE
else
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index 5212abe02c0..bbd5e6e4536 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -73,8 +73,6 @@
update_health_hud()
med_hud_set_health()
med_hud_set_status()
- if(!gibbed && !QDELETED(src))
- addtimer(CALLBACK(src, PROC_REF(med_hud_set_status)), DEFIB_TIME_LIMIT + 1)
GLOB.alive_mob_list -= src
GLOB.dead_mob_list += src
diff --git a/code/modules/mob/living/living_status_procs.dm b/code/modules/mob/living/living_status_procs.dm
index 522eceb22b0..70accbf4878 100644
--- a/code/modules/mob/living/living_status_procs.dm
+++ b/code/modules/mob/living/living_status_procs.dm
@@ -845,6 +845,12 @@ STATUS EFFECTS
singlemutcheck(src, block, MUTCHK_FORCED)
dna.UpdateSE()
+// Revivability
+
+
+/mob/living/proc/is_revivable()
+ return has_status_effect(/datum/status_effect/limited_bonus/revivable)
+
///////////////////////////////// FROZEN /////////////////////////////////////
/mob/living/proc/IsFrozen()