diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 531ff4dd937..05bb9fe13ea 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -1016,6 +1016,8 @@
#define COMSIG_HUMAN_SPECIES_CHANGED "human_species_changed"
/// Source: /mob/living/carbon/human/handle_environment(datum/gas_mixture/environment)
#define COMSIG_HUMAN_EARLY_HANDLE_ENVIRONMENT "human_early_handle_environment"
+/// 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/proc/check_shields(): (atom/hit_by, damage, attack_text, attack_type, armour_penetration, damage_type)
#define COMSIG_HUMAN_CHECK_SHIELDS "human_check_shields"
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index cc35d430294..f51475ec50e 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -388,7 +388,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/span.dm b/code/__DEFINES/span.dm
index 951ed9c1f51..44bc7a00dcb 100644
--- a/code/__DEFINES/span.dm
+++ b/code/__DEFINES/span.dm
@@ -140,6 +140,7 @@
#define span_adminticket(str) ("" + str + "")
#define span_error(str) ("" + str + "")
#define span_gamesay(str) ("" + str + "")
+#define span_whisper(str) ("" + str + "")
#define span_wingdings(str) ("" + str + "")
#define span_disarm(str) ("" + str + "")
#define span_heavybrass(str) ("" + str + "")
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index 3baf94e3708..e8234efd019 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -209,3 +209,6 @@
/// Makes you lean on something
#define STATUS_EFFECT_LEANING /datum/status_effect/leaning
+
+/// This status effect allows a mob to be revived with a defibrillator.
+#define STATUS_EFFECT_REVIVABLE /datum/status_effect/limited_bonus/revivable
diff --git a/code/datums/components/defibrillator.dm b/code/datums/components/defibrillator.dm
index 9456b0dcfe3..2b2d9d43fae 100644
--- a/code/datums/components/defibrillator.dm
+++ b/code/datums/components/defibrillator.dm
@@ -227,7 +227,7 @@
// Run through some quick failure states after shocking.
var/time_dead = world.time - target.timeofdeath
- if((time_dead > DEFIB_TIME_LIMIT) || !target.get_organ_slot(INTERNAL_ORGAN_HEART))
+ if(!target.is_revivable() || !target.get_organ_slot(INTERNAL_ORGAN_HEART))
defib_ref.atom_say("Реанимация не удалась - обнаружены необратимые повреждения сердца!")
defib_success = FALSE
else if(target.getBruteLoss() >= 180 || target.getFireLoss() >= 180 || target.getCloneLoss() >= 180)
@@ -258,7 +258,7 @@
target.heal_damages(tox = heal_amount, oxy = 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)
@@ -276,6 +276,10 @@
SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK, 100)
if(ishuman(target.pulledby)) // for some reason, pulledby isnt a list despite it being possible to be pulled by multiple people
excess_shock(user, target, target.pulledby, defib_ref)
+ if(target.receiving_cpr_from)
+ var/mob/living/carbon/human/H = locateUID(target.receiving_cpr_from)
+ if(istype(H))
+ excess_shock(user, target, H, defib_ref)
target.med_hud_set_health()
target.med_hud_set_status()
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index f32221f66fb..7de472866a2 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -179,6 +179,59 @@
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
+
+/datum/status_effect/limited_bonus/revivable/on_creation(mob/living/new_owner, ...)
+ . = ..()
+
+ RegisterSignal(owner, COMSIG_HUMAN_RECEIVE_CPR, PROC_REF(on_cpr))
+ RegisterSignal(owner, COMSIG_LIVING_REVIVE, PROC_REF(on_revive))
+ owner.med_hud_set_status() // update revivability after adding the status effect
+
+/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/adaptive_learning
id = "adaptive_learning"
duration = 300
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index af1d2d2586d..b1ed249de9d 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -207,7 +207,7 @@
var/mob/living/simple_animal/borer/B = has_brain_worms()
// To the right of health bar
if(stat == DEAD || HAS_TRAIT(src, TRAIT_FAKEDEATH))
- var/revivable = timeofdeath && (round(world.time - timeofdeath) < DEFIB_TIME_LIMIT)
+ var/revivable = timeofdeath && is_revivable()
if(!ghost_can_reenter() || suiciding) // DNR or AntagHUD or Suicide
revivable = FALSE
if(revivable)
diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm
index 007ea0067c2..484f2cc2221 100644
--- a/code/game/objects/items/devices/scanners.dm
+++ b/code/game/objects/items/devices/scanners.dm
@@ -605,7 +605,7 @@ REAGENT SCANNER
if(H.timeofdeath)
data["timeofdeath"] = "[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)
data["timetodefib"] = "[DisplayTimeText(tdelta)]"
data["timetodefibText"] = "Дефибриляция возможна!"
else
@@ -811,7 +811,7 @@ REAGENT SCANNER
if(H.timeofdeath && (H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH)))
scan_data += "Время смерти: [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)
scan_data += " Субъект умер [DisplayTimeText(tdelta)] назад"
scan_data += " Дефибриляция возможна!"
else
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index b8b9798cd3e..b8f573bd5b3 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -417,6 +417,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
can_reenter_corpse = FALSE
if(!QDELETED(mind.current)) // Could change while they're choosing
+ mind.current.remove_status_effect(STATUS_EFFECT_REVIVABLE)
mind.current.med_hud_set_status()
SEND_SIGNAL(mind.current, COMSIG_LIVING_SET_DNR)
diff --git a/code/modules/mob/living/carbon/alien/alien_defense.dm b/code/modules/mob/living/carbon/alien/alien_defense.dm
index cf1e4cd59ec..3c174a32b64 100644
--- a/code/modules/mob/living/carbon/alien/alien_defense.dm
+++ b/code/modules/mob/living/carbon/alien/alien_defense.dm
@@ -47,19 +47,22 @@
/mob/living/carbon/alien/attack_hand(mob/living/carbon/human/M)
if(..()) //to allow surgery to return properly.
- return 0
+ return FALSE
switch(M.a_intent)
if(INTENT_HELP)
- help_shake_act(M)
+ if(M.on_fire)
+ pat_out(M)
+ else
+ help_shake_act(M)
if(INTENT_GRAB)
grabbedby(M)
if(INTENT_HARM)
M.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
if(INTENT_DISARM)
M.do_attack_animation(src, ATTACK_EFFECT_DISARM)
- return 1
- return 0
+ return TRUE
+ return FALSE
/mob/living/carbon/alien/attack_animal(mob/living/simple_animal/M)
@@ -70,7 +73,7 @@
/mob/living/carbon/alien/acid_act(acidpwr, acid_volume)
- return 0 //aliens are immune to acid.
+ return FALSE //aliens are immune to acid.
/mob/living/carbon/alien/attack_slime(mob/living/simple_animal/slime/M)
if(..()) //successful slime attack
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 145af8fe9ab..7a3a8aae99b 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -238,22 +238,6 @@
span_notice("Вы трясёте [name], пытаясь разбудить [genderize_ru(gender, "его", "её", "его", "их")]."),\
)
- else if(on_fire)
- var/self_message = span_warning("Вы пытаетесь потушить [name].")
- if(prob(30) && ishuman(M)) // 30% chance of burning your hands
- var/mob/living/carbon/human/H = M
- var/protected = FALSE // Protected from the fire
- if((H.gloves?.max_heat_protection_temperature > 360) || HAS_TRAIT(H, TRAIT_RESIST_HEAT))
- protected = TRUE
- if(!protected)
- H.apply_damage(5, BURN, def_zone = H.hand ? BODY_ZONE_PRECISE_L_HAND : BODY_ZONE_PRECISE_R_HAND)
- self_message = span_danger("Вы обжигаете свои руки, пытаясь потушить [name]!")
- H.update_icons()
-
- M.visible_message(span_warning("[M] пыта[pluralize_ru(M.gender, "ет", "ют")]ся потушить [name]."), self_message)
- playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
- adjust_fire_stacks(-0.5)
-
// BEGIN HUGCODE - N3X
else
playsound(get_turf(src), 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
@@ -275,6 +259,34 @@
else if(H.w_uniform)
H.w_uniform.add_fingerprint(M)
+/**
+ * Handles patting out a fire on someone.
+ *
+ * Removes 0.5 fire stacks per pat, with a 30% chance of the user burning their hand if they don't have adequate heat resistance.
+ * Arguments:
+ * * src - The mob doing the patting
+ * * target - The mob who is currently on fire
+ */
+/mob/living/carbon/proc/pat_out(mob/living/target)
+ if(target == src) // stop drop and roll, no trying to put out fire on yourself for free.
+ to_chat(src, "Stop drop and roll!")
+ return
+ var/self_message = span_warning("Вы пытаетесь потушить [name].")
+ if(prob(30) && ishuman(src)) // 30% chance of burning your hands
+ var/mob/living/carbon/human/H = src
+ var/protected = FALSE // Protected from the fire
+ if((H.gloves?.max_heat_protection_temperature > 360) || HAS_TRAIT(H, TRAIT_RESIST_HEAT))
+ protected = TRUE
+
+ var/obj/item/organ/external/active_hand = H.get_active_hand()
+ if(active_hand && !protected) // Wouldn't really work without a hand
+ active_hand.external_receive_damage(0, 5)
+ self_message = span_danger("Вы обжигаете свои руки, пытаясь потушить [name]!")
+ H.update_icons()
+
+ target.visible_message(span_warning("[target] пыта[pluralize_ru(target.gender, "ет", "ют")]ся потушить [name]."), self_message)
+ playsound(target, 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1)
+ target.adjust_fire_stacks(-0.5)
/mob/living/carbon/proc/check_self_for_injuries()
var/mob/living/carbon/human/H = src
diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm
index cfea69526dd..9f85c4d6be4 100644
--- a/code/modules/mob/living/carbon/death.dm
+++ b/code/modules/mob/living/carbon/death.dm
@@ -51,6 +51,10 @@
if(!.)
return FALSE
+ if(!gibbed && !HAS_TRAIT(src, TRAIT_FAKEDEATH))
+ // We dont want people who have fake death to have the status effect applied a second time when they actually die
+ apply_status_effect(STATUS_EFFECT_REVIVABLE)
+
if(reagents)
reagents.death_metabolize(src)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index a7cd92d68f8..c429d42bbd6 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -538,7 +538,7 @@
if(hasHUD(usr, EXAMINE_HUD_SECURITY_WRITE) && setcriminal != "Cancel")
found_record = 1
if(R.fields["criminal"] == SEC_RECORD_STATUS_EXECUTE)
- to_chat(usr, "Unable to modify the sec status of a person with an active Execution order. Use a security computer instead.")
+ to_chat(usr, span_warning("Unable to modify the sec status of a person with an active Execution order. Use a security computer instead."))
else
var/rank
if(ishuman(usr))
@@ -555,7 +555,7 @@
break // Git out of the general records
if(!found_record)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["secrecord"])
if(hasHUD(usr, EXAMINE_HUD_SECURITY_READ))
@@ -579,7 +579,7 @@
read = 1
if(!read)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["secrecordComment"])
if(hasHUD(usr, EXAMINE_HUD_SECURITY_READ))
@@ -598,12 +598,12 @@
for(var/c in R.fields["comments"])
to_chat(usr, c)
else
- to_chat(usr, "No comments found")
+ to_chat(usr, span_warning("No comments found"))
if(hasHUD(usr, EXAMINE_HUD_SECURITY_WRITE))
to_chat(usr, "\[Add comment\]")
if(!read)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["secrecordadd"])
if(usr.incapacitated() || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || !hasHUD(usr, EXAMINE_HUD_SECURITY_WRITE))
@@ -638,7 +638,7 @@
sec_hud_set_security_status()
if(!modified)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["medrecord"])
if(hasHUD(usr, EXAMINE_HUD_MEDICAL))
@@ -663,7 +663,7 @@
read = 1
if(!read)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["medrecordComment"])
if(hasHUD(usr, EXAMINE_HUD_MEDICAL))
@@ -682,11 +682,11 @@
for(var/c in R.fields["comments"])
to_chat(usr, c)
else
- to_chat(usr, "No comment found")
+ to_chat(usr, span_warning("No comment found"))
to_chat(usr, "\[Add comment\]")
if(!read)
- to_chat(usr, "Unable to locate a data core entry for this person.")
+ to_chat(usr, span_warning("Unable to locate a data core entry for this person."))
if(href_list["medrecordadd"])
if(usr.incapacitated() || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || !hasHUD(usr, EXAMINE_HUD_MEDICAL))
@@ -710,7 +710,7 @@
skills = E.fields["notes"]
break
if(skills)
- to_chat(usr, "Employment records: [skills]\n")
+ to_chat(usr, span_deptradio("Employment records: [skills]\n"))
if(href_list["lookitem"])
var/obj/item/I = locate(href_list["lookitem"])
@@ -779,7 +779,7 @@
/mob/living/carbon/human/proc/play_xylophone()
if(!src.xylophone)
- visible_message("[src] begins playing [p_their()] ribcage like a xylophone. It's quite spooky.","You begin to play a spooky refrain on your ribcage.","You hear a spooky xylophone melody.")
+ visible_message(span_warning("[src] begins playing [p_their()] ribcage like a xylophone. It's quite spooky."), span_notice("You begin to play a spooky refrain on your ribcage."), span_warning("You hear a spooky xylophone melody."))
var/song = pick('sound/effects/xylophone1.ogg','sound/effects/xylophone2.ogg','sound/effects/xylophone3.ogg')
playsound(loc, song, 50, 1, -1)
xylophone = 1
@@ -813,7 +813,7 @@
if(!. && error_msg && user)
if(!fail_msg)
fail_msg = "There is no exposed flesh or thin material [target_zone == BODY_ZONE_HEAD ? "on [p_their()] head" : "on [p_their()] body"] to inject into."
- to_chat(user, "[fail_msg]")
+ to_chat(user, span_alert("[fail_msg]"))
/mob/living/carbon/human/check_obscured_slots(check_transparent)
@@ -961,16 +961,16 @@
if(usr == src)
self = 1
if(!self)
- usr.visible_message("[usr] kneels down, puts [usr.p_their()] hand on [src]'s wrist and begins counting [p_their()] pulse.",\
+ usr.visible_message(span_notice("[usr] kneels down, puts [usr.p_their()] hand on [src]'s wrist and begins counting [p_their()] pulse."),\
"You begin counting [src]'s pulse")
else
- usr.visible_message("[usr] begins counting [p_their()] pulse.",\
+ usr.visible_message(span_notice("[usr] begins counting [p_their()] pulse."),\
"You begin counting your pulse.")
if(src.pulse)
- to_chat(usr, "[self ? "You have a" : "[src] has a"] pulse! Counting...")
+ to_chat(usr, span_notice("[self ? "You have a" : "[src] has a"] pulse! Counting..."))
else
- to_chat(usr, "[src] has no pulse!")//it is REALLY UNLIKELY that a dead person would check his own pulse
+ to_chat(usr, span_warning("[src] has no pulse!"))//it is REALLY UNLIKELY that a dead person would check his own pulse
return
@@ -980,7 +980,7 @@
if(usr.l_move_time >= time) //checks if our mob has moved during the sleep()
to_chat(usr, "You moved while counting. Try again.")
else
- to_chat(usr, "[self ? "Your" : "[src]'s"] pulse is [src.get_pulse(GETPULSE_HAND)].")
+ to_chat(usr, span_notice("[self ? "Your" : "[src]'s"] pulse is [src.get_pulse(GETPULSE_HAND)]."))
/**
@@ -1272,18 +1272,18 @@
if(usr != src)
return 0 //something is terribly wrong
if(incapacitated())
- to_chat(src, "You can't write on the floor in your current state!")
+ to_chat(src, span_warning("You can't write on the floor in your current state!"))
return
if(!bloody_hands)
remove_verb(src, /mob/living/carbon/human/proc/bloody_doodle)
if(gloves)
- to_chat(src, "[gloves] are preventing you from writing anything down!")
+ to_chat(src, span_warning("[gloves] are preventing you from writing anything down!"))
return
var/turf/simulated/T = loc
if(!istype(T)) //to prevent doodling out of mechs and lockers
- to_chat(src, "You cannot reach the floor.")
+ to_chat(src, span_warning("You cannot reach the floor."))
return
var/turf/origin = T
@@ -1291,21 +1291,21 @@
if(direction != "Here")
T = get_step(T,text2dir(direction))
if(!istype(T))
- to_chat(src, "You cannot doodle there.")
+ to_chat(src, span_warning("You cannot doodle there."))
return
var/num_doodles = 0
for(var/obj/effect/decal/cleanable/blood/writing/W in T)
num_doodles++
if(num_doodles > 4)
- to_chat(src, "There is no space to write on!")
+ to_chat(src, span_warning("There is no space to write on!"))
return
var/max_length = bloody_hands * 30 //tweeter style
var/message = tgui_input_text(src, "Write a message. It cannot be longer than [max_length] characters.", "Blood writing", max_length = max_length)
if(origin != loc)
- to_chat(src, "Stay still while writing!")
+ to_chat(src, span_notice("Stay still while writing!"))
return
if(message)
var/used_blood_amount = round(length(message) / 30, 1)
@@ -1313,9 +1313,9 @@
if(length(message) > max_length)
message += "-"
- to_chat(src, "You ran out of blood to write with!")
+ to_chat(src, span_warning("You ran out of blood to write with!"))
else
- to_chat(src, "You daub '[message]' on [T] in shiny red lettering.")
+ to_chat(src, span_notice("You daub '[message]' on [T] in shiny red lettering."))
var/obj/effect/decal/cleanable/blood/writing/W = new(T)
W.message = message
W.add_fingerprint(src)
@@ -1451,7 +1451,7 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
for(var/obj/item/hand in handlist)
if(prob(current_size * 5) && hand.w_class >= ((11-current_size)/2) && drop_item_ground(hand))
step_towards(hand, src)
- to_chat(src, "\The [S] pulls \the [hand] from your grip!")
+ to_chat(src, span_warning("\The [S] pulls \the [hand] from your grip!"))
apply_effect(current_size * 3, IRRADIATE)
/mob/living/carbon/human/narsie_act(obj/singularity/god/narsie/narsie)
@@ -1466,45 +1466,145 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
ratvar.soul_devoured++
. = ..()
+
+#define CPR_CHEST_COMPRESSION_ONLY 0.75
+#define CPR_RESCUE_BREATHS 1
+#define CPR_START_HEART_CHANCE 25
+#define CPR_CHEST_COMPRESSION_RESTORATION (1 SECONDS)
+#define CPR_BREATHS_RESTORATION (3 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!")
+ to_chat(src, span_warning("You cannot perform CPR on yourself!"))
return
- if(H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH))
- to_chat(src, "[H.name] is dead!")
+ if(!isnull(H.receiving_cpr_from)) // To prevent spam stacking
+ to_chat(src, span_warning("They are already receiving CPR!"))
return
- if(!check_has_mouth())
- to_chat(src, "You don't have a mouth, you cannot perform CPR!")
- return
- if(!H.check_has_mouth())
- to_chat(src, "They don't have a mouth, you cannot perform CPR!")
+ if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) || !has_both_hands() || pulling && pull_hand != PULL_WITHOUT_HANDS)
+ to_chat(src, span_warning("You need two hands available to do CPR!"))
return
- if((head && (head.flags_cover & HEADCOVERSMOUTH)) || (wear_mask && (wear_mask.flags_cover & MASKCOVERSMOUTH) && !wear_mask.up))
- to_chat(src, "Remove your mask first!")
+ if(l_hand || r_hand)
+ to_chat(src, span_warning("You can't perform effective CPR with your hands full!"))
return
- if((H.head && (H.head.flags_cover & HEADCOVERSMOUTH)) || (H.wear_mask && (H.wear_mask.flags_cover & MASKCOVERSMOUTH) && !H.wear_mask.up))
- to_chat(src, "Remove [H.p_their()] mask first!")
- return
- if(H.receiving_cpr) // To prevent spam stacking
- to_chat(src, "They are already receiving CPR!")
+
+ H.receiving_cpr_from = UID()
+ var/cpr_modifier = get_cpr_mod(H)
+ if(H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH))
+ if(ismachineperson(H) && do_after(src, 4 SECONDS, H, NONE)) // hehe
+ visible_message(
+ span_warning("[src] bangs [p_their()] head on [H]'s chassis by accident!"),
+ span_danger("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)
+ apply_damage(2, BRUTE, BODY_ZONE_HEAD)
+ H.receiving_cpr_from = null
+ return
+
+ if(!H.is_revivable())
+ to_chat(src, span_warning("[H] is already too far gone for CPR..."))
+ H.receiving_cpr_from = null
+ return
+
+ visible_message(span_danger("[src] is trying to perform CPR on [H]'s lifeless body!"), span_danger("You start trying to perform CPR on [H]'s lifeless body!"))
+ while(do_after(src, 4 SECONDS, H, NONE) && (H.stat == DEAD || HAS_TRAIT(H, TRAIT_FAKEDEATH)) && H.is_revivable())
+ var/timer_restored
+ if(cpr_modifier == CPR_CHEST_COMPRESSION_ONLY)
+ visible_message(span_notice("[src] gives [H] chest compressions."), span_notice("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(span_notice("[src] gives [H] chest compressions and rescue breaths."), span_notice("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, span_userdanger("Your chest burns as you receive unnecessary CPR!"))
+ continue
+
+ SEND_SIGNAL(H, COMSIG_HUMAN_RECEIVE_CPR, timer_restored)
+
+ if(prob(5))
+ if(timer_restored >= CPR_BREATHS_RESTORATION)
+ to_chat(src, pick(effective_cpr_messages))
+ else
+ to_chat(src, pick(ineffective_cpr_messages))
+
+ cpr_try_activate_bomb(H)
+
+
+ if(!H.is_revivable())
+ to_chat(src, span_notice("You feel [H]'s body is already starting to stiffen beneath you...it's too late for CPR now."))
+ else
+ visible_message(span_notice("[src] stops giving [H] CPR."), span_notice("You stop giving [H] CPR."))
+
+ H.receiving_cpr_from = null
return
- visible_message("[src] is trying to perform CPR on [H.name]!", "You try to perform CPR on [H.name]!")
- H.receiving_cpr = TRUE
- if(do_after(src, 4 SECONDS, H, NONE))
- if(H.health <= HEALTH_THRESHOLD_CRIT)
- H.heal_damage_type(15, OXY)
- H.SetLoseBreath(0)
- H.AdjustParalysis(-2 SECONDS)
- visible_message("[src] performs CPR on [H.name]!", "You perform CPR on [H.name].")
-
- to_chat(H, "You feel a breath of fresh air enter your lungs. It feels good.")
- H.receiving_cpr = FALSE
- add_attack_logs(src, H, "CPRed", ATKLOG_ALL)
- return TRUE
- else
- H.receiving_cpr = FALSE
- to_chat(src, "You need to stay still while performing CPR!")
+ visible_message(span_danger("[src] is trying to perform CPR on [H.name]!"), span_danger("You try to perform CPR on [H.name]!"))
+
+ if(cpr_modifier == CPR_CHEST_COMPRESSION_ONLY)
+ to_chat(src, span_warning("You can't get to [H]'s mouth, so your CPR will be less effective!"))
+
+ while(do_after(src, 4 SECONDS, H, NONE) && H.health <= HEALTH_THRESHOLD_CRIT)
+ H.adjustOxyLoss(-15 * cpr_modifier)
+ H.SetLoseBreath(0)
+ H.AdjustParalysis(-2 SECONDS)
+ H.updatehealth("cpr")
+ visible_message(span_danger("[src] performs CPR on [H.name]!"), span_notice("You perform CPR on [H.name]."))
+
+ if(prob(CPR_START_HEART_CHANCE))
+ set_heartattack(FALSE)
+
+ cpr_try_activate_bomb(H)
+
+ if(cpr_modifier == CPR_RESCUE_BREATHS)
+ to_chat(H, span_notice("You feel a breath of fresh air enter your lungs. It feels good."))
+
+ add_attack_logs(src, H, "CPRed", ATKLOG_ALL)
+
+ H.receiving_cpr_from = null
+ visible_message(span_notice("[src] stops performing CPR on [H]."), span_notice("You stop performing CPR on [H]."))
+ to_chat(src, span_danger("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_NO_BREATH) || HAS_TRAIT(H, TRAIT_NO_BREATH))
+ return CPR_CHEST_COMPRESSION_ONLY
+
+ if(dna?.species.breathid != H.dna?.species.breathid) // sorry non oxy-breathers
+ to_chat(src, span_warning("You don't think you'd be able to give [H] very effective rescue breaths..."))
+ return CPR_CHEST_COMPRESSION_ONLY
+ return CPR_RESCUE_BREATHS
+
+/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, span_notice("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_START_HEART_CHANCE
+#undef CPR_CHEST_COMPRESSION_RESTORATION
+#undef CPR_BREATHS_RESTORATION
/mob/living/carbon/human/has_mutated_organs()
for(var/obj/item/organ/external/E as anything in bodyparts)
@@ -1795,7 +1895,7 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
/mob/living/carbon/human/get_spooked()
- to_chat(src, "[pick(GLOB.boo_phrases)]")
+ to_chat(src, span_whisper("[pick(GLOB.boo_phrases)]"))
return TRUE
/mob/living/carbon/human/extinguish_light(force = FALSE)
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index 040145b9b64..b26e19cb5cd 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -75,7 +75,8 @@
var/bleedsuppress = 0 //for stopping bloodloss
var/heartbeat = 0
- var/receiving_cpr = FALSE
+ /// UID of the person who is giving this mob CPR.
+ var/receiving_cpr_from
var/datum/body_accessory/body_accessory = null
/// Name of tail image in species effects icon file.
diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm
index 5697cb4ee95..964f851666a 100644
--- a/code/modules/mob/living/carbon/human/species/_species.dm
+++ b/code/modules/mob/living/carbon/human/species/_species.dm
@@ -487,7 +487,9 @@
/datum/species/proc/help(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style)
if(attacker_style && attacker_style.help_act(user, target) == TRUE)//adminfu only...
return TRUE
- if(target.health >= HEALTH_THRESHOLD_CRIT && !HAS_TRAIT(target, TRAIT_FAKEDEATH))
+ if(target.on_fire)
+ user.pat_out(target)
+ 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 762380b619b..badd6087f5f 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -80,8 +80,6 @@
update_stamina_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)
for(var/s in ownedSoullinks)
var/datum/soullink/S = s
diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm
index 1f3b8ad74e8..06864332a28 100644
--- a/code/modules/mob/living/init_signals.dm
+++ b/code/modules/mob/living/init_signals.dm
@@ -70,12 +70,14 @@
/mob/living/proc/on_fakedeath_trait_gain(datum/source)
SIGNAL_HANDLER
ADD_TRAIT(src, TRAIT_KNOCKEDOUT, TRAIT_FAKEDEATH)
+ apply_status_effect(STATUS_EFFECT_REVIVABLE)
/// Called when [TRAIT_FAKEDEATH] is removed from the mob.
/mob/living/proc/on_fakedeath_trait_loss(datum/source)
SIGNAL_HANDLER
REMOVE_TRAIT(src, TRAIT_KNOCKEDOUT, TRAIT_FAKEDEATH)
+ remove_status_effect(STATUS_EFFECT_REVIVABLE)
/// Called when [TRAIT_IMMOBILIZED] is added to the mob.
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index b9b2db9bafa..5bac1d553a0 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -910,6 +910,12 @@
force_gene_block(block, FALSE)
+// Revivability
+
+/mob/living/proc/is_revivable()
+ return has_status_effect(STATUS_EFFECT_REVIVABLE)
+
+
///Unignores all slowdowns that lack the IGNORE_NOSLOW flag.
/mob/living/proc/unignore_slowdown(source)
REMOVE_TRAIT(src, TRAIT_IGNORESLOWDOWN, source)
diff --git a/sound/weapons/ringslam.ogg b/sound/weapons/ringslam.ogg
new file mode 100644
index 00000000000..5c2d7f339b6
Binary files /dev/null and b/sound/weapons/ringslam.ogg differ