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" />