From f923f9f2f5d108a79e30993eb1e8abda16f708b2 Mon Sep 17 00:00:00 2001
From: Drathek <76988376+Drulikar@users.noreply.github.com>
Date: Sat, 6 Apr 2024 03:53:35 -0700
Subject: [PATCH] Cryo Tube Rework (#6058)
# About the pull request
This PR reworks the cryotubes. The effect of diluting or multiplying
chemicals is no longer possible, but now the use of cryoxadone or
clonexadone in a cryotube can allow chemicals to process in the dead.
Ultimately usage of the cryo chemicals will be much higher, and you need
to be careful not to introduce too many chemicals into your patient
because the dosage is 5u whenever cryoxadone or clonexadone is available
and can be administered.
Cryotubes also now actually require power, update their icon if they
lose power, and use the two-ping sfx for warnings (such as now warnings
as the patient's revive timer is running out). For the critical
messages, it will make an alert if the patient is dead, then once they
hit the 50% mark (2.5 mins), then at the 1 minute mark.
See testing for a demonstration.
# Explain why it's good for the game
Cryotubes in general are very underused with the exception of exploiting
how it can multiply chemicals. This should give them a unique feature to
medbay.
It is likely that this may prove to be too powerful, but it puts more
demand on chemistry to provide the cryoxadone or clonexadone in order to
do so.
# Testing Photographs and Procedure
Screenshots & Videos
Initial testing:
https://youtu.be/PA43YvzDRVM
Critical (dead state) alert testing:
https://youtu.be/PLjQxzv28l8
Notification changes:
![release](https://github.com/cmss13-devs/cmss13/assets/76988376/325e852a-79ee-4a68-9c3c-cd9001f6cc0f)
Power fixes:
![power](https://github.com/cmss13-devs/cmss13/assets/76988376/afd952eb-3004-4c91-b255-83aa98fd539f)
# Changelog
:cl: Drathek
balance: Cryotubes no longer multiply chemicals.
balance: Cryotubes can now allow chemicals to process in dead if
cryoxadone or clonexadone is present.
fix: Fixed cryotubes not actually updating their icons/power usage when
they become unpowered.
fix: Fixed cryotubes healing despite inoperable.
fix: The cryotube Eject Occupant verb now works for people other than
the occupant (useful if inoperable)
ui: Cryotubes can optionally announce again on the medical channel.
/:cl:
---
code/game/machinery/cryo.dm | 193 ++++++++++++------
code/game/objects/objs.dm | 52 +++--
.../human/life/handle_chemicals_in_body.dm | 30 ++-
tgui/packages/tgui/interfaces/Cryo.jsx | 27 ++-
4 files changed, 201 insertions(+), 101 deletions(-)
diff --git a/code/game/machinery/cryo.dm b/code/game/machinery/cryo.dm
index afcc9686cff5..6643cd6b805c 100644
--- a/code/game/machinery/cryo.dm
+++ b/code/game/machinery/cryo.dm
@@ -1,4 +1,8 @@
#define HEAT_CAPACITY_HUMAN 100 //249840 J/K, for a 72 kg person.
+#define DEATH_STAGE_NONE 0
+#define DEATH_STAGE_EARLY 1
+#define DEATH_STAGE_WARNING 2
+#define DEATH_STAGE_CRITICAL 3
/obj/structure/machinery/cryo_cell
name = "cryo cell"
@@ -19,6 +23,7 @@
var/mob/living/carbon/occupant = null
var/obj/item/reagent_container/glass/beaker = null
+ var/occupant_death_stage = DEATH_STAGE_NONE
/obj/structure/machinery/cryo_cell/Initialize()
. = ..()
@@ -28,19 +33,18 @@
QDEL_NULL(beaker)
. = ..()
-
/obj/structure/machinery/cryo_cell/process()
if(!on)
updateUsrDialog()
return
if(occupant)
- if(occupant.stat != DEAD)
- process_occupant()
- else
+ var/mob/living/carbon/human/human_occupant = occupant
+ if(occupant.stat == DEAD && (!istype(human_occupant) || human_occupant.undefibbable))
go_out(TRUE, TRUE) //Whether auto-eject is on or not, we don't permit literal deadbeats to hang around.
- playsound(src.loc, 'sound/machines/ping.ogg', 25, 1)
- visible_message("[icon2html(src, viewers(src))] [SPAN_WARNING("\The [src] pings: Patient is dead!")]")
+ display_message("Patient is dead!", warning = TRUE)
+ else
+ process_occupant()
updateUsrDialog()
return TRUE
@@ -116,13 +120,14 @@
data["beakerContents"] = beakerContents
return data
-/obj/structure/machinery/cryo_cell/ui_act(action, list/params)
+/obj/structure/machinery/cryo_cell/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("power")
on = !on
+ update_use_power(on ? USE_POWER_ACTIVE : USE_POWER_IDLE)
update_icon()
. = TRUE
if("eject")
@@ -143,7 +148,8 @@
if("notice")
release_notice = !release_notice
. = TRUE
- updateUsrDialog()
+
+ updateUsrDialog(ui.user)
/obj/structure/machinery/cryo_cell/attackby(obj/item/W, mob/living/user)
if(istype(W, /obj/item/reagent_container/glass))
@@ -158,34 +164,59 @@
beaker = W
var/reagentnames = ""
- for(var/datum/reagent/R in beaker.reagents.reagent_list)
- reagentnames += ";[R.name]"
+ for(var/datum/reagent/cur_reagent in beaker.reagents.reagent_list)
+ reagentnames += ";[cur_reagent.name]"
- msg_admin_niche("[key_name(user)] put \a [beaker] into \the [src], containing [reagentnames] at ([src.loc.x],[src.loc.y],[src.loc.z]) [ADMIN_JMP(src.loc)].", 1)
+ msg_admin_niche("[key_name(user)] put \a [beaker] into [src], containing [reagentnames] at ([src.loc.x],[src.loc.y],[src.loc.z]) [ADMIN_JMP(src.loc)].", 1)
if(user.drop_inv_item_to_loc(W, src))
- user.visible_message("[user] adds \a [W] to \the [src]!", "You add \a [W] to \the [src]!")
+ user.visible_message("[user] adds \a [W] to [src]!", "You add \a [W] to [src]!")
else if(istype(W, /obj/item/grab))
- if(isxeno(user)) return
- var/obj/item/grab/G = W
- if(!ismob(G.grabbed_thing))
+ if(isxeno(user))
+ return
+ var/obj/item/grab/grabber = W
+ if(!ismob(grabber.grabbed_thing))
return
- var/mob/M = G.grabbed_thing
- put_mob(M)
+ var/mob/grabbed_mob = grabber.grabbed_thing
+ put_mob(grabbed_mob)
- updateUsrDialog()
+ updateUsrDialog(user)
+/obj/structure/machinery/cryo_cell/power_change(area/master_area)
+ . = ..()
+ if((occupant || on) && operable())
+ update_use_power(USE_POWER_ACTIVE)
+ update_icon()
/obj/structure/machinery/cryo_cell/update_icon()
icon_state = initial(icon_state)
- icon_state = "[icon_state]-[on ? "on" : "off"]-[occupant ? "occupied" : "empty"]"
+ var/is_on = on && operable()
+ icon_state = "[icon_state]-[is_on ? "on" : "off"]-[occupant ? "occupied" : "empty"]"
/obj/structure/machinery/cryo_cell/proc/process_occupant()
- if(occupant)
- if(occupant.stat == DEAD)
- return
- occupant.bodytemperature += 2*(temperature - occupant.bodytemperature)
- occupant.bodytemperature = max(occupant.bodytemperature, temperature) // this is so ugly i'm sorry for doing it i'll fix it later i promise
+ if(!occupant)
+ return
+ if(!operable())
+ return
+
+ occupant.bodytemperature += 2*(temperature - occupant.bodytemperature)
+ occupant.bodytemperature = max(occupant.bodytemperature, temperature) // this is so ugly i'm sorry for doing it i'll fix it later i promise
+
+ // Warnings if dead
+ if(occupant.stat == DEAD && ishuman(occupant))
+ var/mob/living/carbon/human/human_occupant = occupant
+ var/old_state = occupant_death_stage
+ if(world.time > occupant.timeofdeath + human_occupant.revive_grace_period - 1 MINUTES)
+ occupant_death_stage = DEATH_STAGE_CRITICAL
+ else if(world.time > occupant.timeofdeath + human_occupant.revive_grace_period - 2.5 MINUTES)
+ occupant_death_stage = DEATH_STAGE_WARNING
+ else
+ occupant_death_stage = DEATH_STAGE_EARLY
+ if(old_state != occupant_death_stage)
+ display_message("Patient is critical!", warning = TRUE)
+
+ // Passive healing if alive and cold enough
+ if(occupant.stat != DEAD)
occupant.recalculate_move_delay = TRUE
occupant.set_stat(UNCONSCIOUS)
if(occupant.bodytemperature < T0C)
@@ -202,22 +233,39 @@
var/heal_brute = occupant.getBruteLoss() ? min(1, 20/occupant.getBruteLoss()) : 0
var/heal_fire = occupant.getFireLoss() ? min(1, 20/occupant.getFireLoss()) : 0
occupant.heal_limb_damage(heal_brute,heal_fire)
- var/has_cryo = occupant.reagents.get_reagent_amount("cryoxadone") >= 1
- var/has_clonexa = occupant.reagents.get_reagent_amount("clonexadone") >= 1
- var/has_cryo_medicine = has_cryo || has_clonexa
- if(beaker && !has_cryo_medicine)
- beaker.reagents.trans_to(occupant, 1, 10)
+
+ // Chemical healing if cryo meds are involved
+ if(beaker && occupant.reagents && beaker.reagents)
+ var/occupant_has_cryo_meds = occupant.reagents.get_reagent_amount("cryoxadone") >= 1 || occupant.reagents.get_reagent_amount("clonexadone") >= 1
+ var/beaker_has_cryo_meds = beaker.reagents.get_reagent_amount("cryoxadone") >= 1 || beaker.reagents.get_reagent_amount("clonexadone") >= 1
+
+ // To administer, either the occupant has cryo meds and the beaker doesn't or vice versa (not both)
+ var/can_administer = (occupant_has_cryo_meds ^ beaker_has_cryo_meds) && length(beaker.reagents.reagent_list)
+ if(can_administer && occupant_has_cryo_meds)
+ // If its the case of the occupant has cryo meds and not the beaker, we need to pace out the dosage
+ // So lets make sure they don't already have some of the beaker drugs
+ for(var/datum/reagent/cur_beaker_reagent in beaker.reagents.reagent_list)
+ for(var/datum/reagent/cur_occupant_reagent in occupant.reagents.reagent_list)
+ if(cur_beaker_reagent.id == cur_occupant_reagent.id)
+ can_administer = FALSE
+ break
+
+ if(can_administer)
+ beaker.reagents.trans_to(occupant, 5)
beaker.reagents.reaction(occupant)
- if(!occupant.getBruteLoss(TRUE) && !occupant.getFireLoss(TRUE) && !occupant.getCloneLoss() && autoeject) //release the patient automatically when brute and burn are handled on non-robotic limbs
- display_message("external wounds are")
+
+ if(autoeject)
+ //release the patient automatically when brute and burn are handled on non-robotic limbs
+ if(!occupant.getBruteLoss(TRUE) && !occupant.getFireLoss(TRUE) && !occupant.getCloneLoss())
+ display_message("Patient's external wounds are healed.")
go_out(TRUE)
return
- if(occupant.health >= 100 && autoeject)
- display_message("external wounds are")
+ if(occupant.health >= occupant.maxHealth)
+ display_message("Patient's external wounds are healed.")
go_out(TRUE)
return
-/obj/structure/machinery/cryo_cell/proc/go_out(auto_eject = null, dead = null)
+/obj/structure/machinery/cryo_cell/proc/go_out(auto_eject = FALSE, dead = FALSE)
if(!(occupant))
return
if(occupant.client)
@@ -235,66 +283,72 @@
if(occupant.bodytemperature < 261 && occupant.bodytemperature >= 70)
occupant.bodytemperature = 261
occupant.recalculate_move_delay = TRUE
- occupant = null
if(auto_eject) //Turn off and announce if auto-ejected because patient is recovered or dead.
on = FALSE
if(release_notice) //If auto-release notices are on as it should be, let the doctors know what's up
- playsound(src.loc, 'sound/machines/ping.ogg', 100, 14)
- var/reason = "Reason for release: Patient recovery."
+ var/reason = "Reason for release: Patient recovery."
if(dead)
- reason = "Reason for release: Patient death."
- ai_silent_announcement("Patient [occupant] has been automatically released from \the [src] at: [get_area(occupant)]. [reason]", MED_FREQ)
+ reason = "Reason for release: Patient death."
+ ai_silent_announcement("Patient [occupant] has been automatically released from [src] at: [sanitize_area((get_area(occupant))?.name)]. [reason]", ":m")
+ occupant = null
update_use_power(USE_POWER_IDLE)
update_icon()
return
-/obj/structure/machinery/cryo_cell/proc/put_mob(mob/living/carbon/M as mob)
+/obj/structure/machinery/cryo_cell/proc/put_mob(mob/living/carbon/cur_mob)
if(inoperable())
to_chat(usr, SPAN_DANGER("The cryo cell is not functioning."))
return
- if(!istype(M) || isxeno(M))
- to_chat(usr, SPAN_DANGER("The cryo cell cannot handle such a lifeform!"))
+ if(!istype(cur_mob) || isxeno(cur_mob))
+ to_chat(usr, SPAN_DANGER("The cryo cell cannot handle such a lifeform!"))
return
if(occupant)
- to_chat(usr, SPAN_DANGER("The cryo cell is already occupied!"))
+ to_chat(usr, SPAN_DANGER("The cryo cell is already occupied!"))
return
- if(M.abiotic())
+ if(cur_mob.abiotic())
to_chat(usr, SPAN_DANGER("Subject may not have abiotic items on."))
return
- if(do_after(usr, 20, INTERRUPT_NO_NEEDHAND, BUSY_ICON_GENERIC))
- to_chat(usr, SPAN_NOTICE("You move [M.name] inside the cryo cell."))
- M.forceMove(src)
- if(M.health >= -100 && (M.health <= 0 || M.sleeping))
- to_chat(M, SPAN_NOTICE("You feel cold liquid surround you. Your skin starts to freeze up."))
- occupant = M
+ if(do_after(usr, 2 SECONDS, INTERRUPT_NO_NEEDHAND, BUSY_ICON_GENERIC))
+ visible_message(SPAN_NOTICE("[usr] moves [usr == cur_mob ? "" : "[cur_mob] "]inside the cryo cell."))
+ cur_mob.forceMove(src)
+ if(cur_mob.health >= HEALTH_THRESHOLD_DEAD && (cur_mob.health <= 0 || cur_mob.sleeping))
+ to_chat(cur_mob, SPAN_NOTICE("You feel cold liquid surround you. Your skin starts to freeze up."))
+ occupant = cur_mob
+ occupant_death_stage = DEATH_STAGE_NONE
update_use_power(USE_POWER_ACTIVE)
update_icon()
return TRUE
-/obj/structure/machinery/cryo_cell/proc/display_message(msg)
- playsound(src.loc, 'sound/machines/ping.ogg', 25, 1)
- visible_message("[icon2html(src, viewers(src))] [SPAN_NOTICE("\The [src] pings: Patient's " + msg + " healed.")]")
+/obj/structure/machinery/cryo_cell/proc/display_message(msg, silent = FALSE, warning = FALSE)
+ if(!silent)
+ if(warning)
+ playsound(loc, 'sound/machines/twobeep.ogg', 40)
+ else
+ playsound(loc, 'sound/machines/ping.ogg', 25, 1)
+ visible_message("[icon2html(src, viewers(src))] [SPAN_NOTICE("[src] [warning ? "beeps" : "pings"]: [msg]")]")
/obj/structure/machinery/cryo_cell/verb/move_eject()
set name = "Eject occupant"
set category = "Object"
set src in oview(1)
if(usr == occupant)//If the user is inside the tube...
- if(usr.stat == 2)//and he's not dead....
+ if(usr.stat == DEAD)//and he's not dead....
return
- if(alert(usr, "Would you like to activate the ejection sequence of the cryo cell? Healing may be in progress.", "Confirm", "Yes", "No") == "Yes")
+ if(tgui_alert(usr, "Would you like to activate the ejection sequence of the cryo cell? Healing may be in progress.", "Confirm", list("Yes", "No")) == "Yes")
to_chat(usr, SPAN_NOTICE("Cryo cell release sequence activated. This will take thirty seconds."))
- visible_message(SPAN_WARNING ("The cryo cell's tank starts draining as its ejection lights blare!"))
- sleep(300)
- if(!src || !usr || !occupant || (occupant != usr)) //Check if someone's released/replaced/bombed him already
- return
- go_out()//and release him from the eternal prison.
- else
- if(usr.stat != 0)
- return
- go_out()
- return
+ visible_message(SPAN_WARNING("The cryo cell's tank starts draining as its ejection lights blare!"))
+ addtimer(CALLBACK(src, PROC_REF(finish_eject), usr), 30 SECONDS, TIMER_UNIQUE|TIMER_NO_HASH_WAIT)
+ else
+ if(usr.stat != CONSCIOUS)
+ return
+ go_out()
+
+/obj/structure/machinery/cryo_cell/proc/finish_eject(mob/original)
+ //Check if someone's released/replaced/bombed him already
+ if(QDELETED(src) || QDELETED(original) || !occupant || occupant != original)
+ return
+ go_out()//and release him from the eternal prison.
/obj/structure/machinery/cryo_cell/verb/move_inside()
set name = "Move Inside"
@@ -309,8 +363,8 @@
//clickdrag code - "resist to get out" code is in living_verbs.dm
/obj/structure/machinery/cryo_cell/MouseDrop_T(mob/target, mob/user)
. = ..()
- var/mob/living/H = user
- if(!istype(H) || target != user) //cant make others get in. grab-click for this
+ var/mob/living/living_mob = user
+ if(!istype(living_mob) || target != user) //cant make others get in. grab-click for this
return
put_mob(target)
@@ -324,3 +378,8 @@
/datum/data/function/proc/display()
return
+
+#undef DEATH_STAGE_NONE
+#undef DEATH_STAGE_EARLY
+#undef DEATH_STAGE_WARNING
+#undef DEATH_STAGE_CRITICAL
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 8a37eef3ee73..13d1bbaefcf7 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -142,31 +142,39 @@
return "on [t_his] feet"
return "...somewhere?"
-/obj/proc/updateUsrDialog()
- if(in_use)
- var/is_in_use = 0
- var/list/nearby = viewers(1, src)
- for(var/mob/M in nearby)
- if ((M.client && M.interactee == src))
- is_in_use = 1
- attack_hand(M)
- if (isSilicon(usr))
- if (!(usr in nearby))
- if (usr.client && usr.interactee==src) // && M.interactee == src is omitted because if we triggered this by using the dialog, it doesn't matter if our machine changed in between triggering it and this - the dialog is probably still supposed to refresh.
- is_in_use = 1
- attack_remote(usr)
- in_use = is_in_use
+/obj/proc/updateUsrDialog(mob/user)
+ if(!user)
+ user = usr
+ if(!in_use || !user)
+ return
+
+ var/is_in_use = FALSE
+ var/list/nearby = viewers(1, src)
+ for(var/mob/cur_mob in nearby)
+ if(cur_mob.client && cur_mob.interactee == src)
+ is_in_use = TRUE
+ attack_hand(cur_mob)
+ if(isSilicon(user))
+ if(!(user in nearby))
+ if(user.client && user.interactee == src) // && M.interactee == src is omitted because if we triggered this by using the dialog, it doesn't matter if our machine changed in between triggering it and this - the dialog is probably still supposed to refresh.
+ is_in_use = TRUE
+ attack_remote(user)
+
+ in_use = is_in_use
/obj/proc/updateDialog()
// Check that people are actually using the machine. If not, don't update anymore.
- if(in_use)
- var/list/nearby = viewers(1, src)
- var/is_in_use = 0
- for(var/mob/M in nearby)
- if ((M.client && M.interactee == src))
- is_in_use = 1
- src.interact(M)
- in_use = is_in_use
+ if(!in_use)
+ return
+
+ var/is_in_use = FALSE
+ var/list/nearby = viewers(1, src)
+ for(var/mob/cur_mob in nearby)
+ if(cur_mob.client && cur_mob.interactee == src)
+ is_in_use = TRUE
+ interact(cur_mob)
+
+ in_use = is_in_use
/obj/proc/interact(mob/user)
return
diff --git a/code/modules/mob/living/carbon/human/life/handle_chemicals_in_body.dm b/code/modules/mob/living/carbon/human/life/handle_chemicals_in_body.dm
index eafac03fd51f..9bf275a5448a 100644
--- a/code/modules/mob/living/carbon/human/life/handle_chemicals_in_body.dm
+++ b/code/modules/mob/living/carbon/human/life/handle_chemicals_in_body.dm
@@ -39,25 +39,35 @@
SHOULD_NOT_SLEEP(TRUE)
if(!reagents || undefibbable)
return // Double checking due to Life() funny background=1
- for(var/datum/reagent/generated/R in reagents.reagent_list)
+
+ var/has_cryo_medicine = reagents.get_reagent_amount("cryoxadone") >= 1 || reagents.get_reagent_amount("clonexadone") >= 1
+ if(has_cryo_medicine)
+ var/obj/structure/machinery/cryo_cell/cryo = loc
+ if(!istype(cryo) || !cryo.on || cryo.inoperable())
+ has_cryo_medicine = FALSE
+
+ for(var/datum/reagent/cur_reagent in reagents.reagent_list)
+ if(!has_cryo_medicine && !istype(cur_reagent, /datum/reagent/generated))
+ continue
+
var/list/mods = list( REAGENT_EFFECT = TRUE,
REAGENT_BOOST = FALSE,
REAGENT_PURGE = FALSE,
- REAGENT_FORCE = FALSE,
+ REAGENT_FORCE = has_cryo_medicine,
REAGENT_CANCEL = FALSE)
- for(var/datum/chem_property/P in R.properties)
- var/list/A = P.pre_process(src)
- if(!A)
+ for(var/datum/chem_property/cur_prop in cur_reagent.properties)
+ var/list/results = cur_prop.pre_process(src)
+ if(!results)
continue
- for(var/mod in A)
- mods[mod] |= A[mod]
+ for(var/mod in results)
+ mods[mod] |= results[mod]
if(mods[REAGENT_CANCEL])
return
if(mods[REAGENT_FORCE])
- R.handle_processing(src, mods, delta_time)
- R.holder.remove_reagent(R.id, R.custom_metabolism * delta_time)
+ cur_reagent.handle_processing(src, mods, delta_time)
+ cur_reagent.holder.remove_reagent(cur_reagent.id, cur_reagent.custom_metabolism * delta_time)
- R.handle_dead_processing(src, mods, delta_time)
+ cur_reagent.handle_dead_processing(src, mods, delta_time)
diff --git a/tgui/packages/tgui/interfaces/Cryo.jsx b/tgui/packages/tgui/interfaces/Cryo.jsx
index 338717f2d0ca..be1dce801ada 100644
--- a/tgui/packages/tgui/interfaces/Cryo.jsx
+++ b/tgui/packages/tgui/interfaces/Cryo.jsx
@@ -34,6 +34,11 @@ export const Cryo = () => {
const CryoContent = (props) => {
const { act, data } = useBackend();
+
+ let soundicon = 'volume-high';
+ if (!data.notify) {
+ soundicon = 'volume-xmark';
+ }
return (
<>
@@ -89,12 +94,30 @@ const CryoContent = (props) => {
icon="eject"
disabled={!data.hasOccupant}
onClick={() => act('eject')}
- content="eject patient"
+ content="Eject Patient"
/>