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()