From 41ec21dda5d9b7c276dba7ff1d52c9f55d1d7bb2 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:00:06 +0100 Subject: [PATCH] [MIRROR] Chemical Reaction Tester/Debugger 2.0 (#26244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * General maintenance for cryo chamber (#80873) The changes can be summarized into the following categories 1. **Qol**(Should not be a problem for feature freeze hopefully) - Adds balloon alerts for screwdriver & wrench acts, when inserting a beaker and other stuff - Adds extra examines for the beaker inside the cryo, how to open maintenance panel & other stuff. Most of the examines were not enclosed in `span_notice` - Adds screen tips for crowbar act, when inserting beaker into cryo etc - Cryo can now be pried open when there is no power with a crowbar so you can free someone stuck inside in case of an emergency - Cryo now auto turns on immediately without a 2 second delay & without the need for auto eject 2. **Fixes** - Cryo won't drop the beaker onto the floor when ejected, but will put it in the players hands as intended - Fixes #80699. The cryo was on `ABOVE_MOB_LAYER` which rendered on top of everything, Now its set to the default layer so mobs & other stuff now render on top of it - Loc checks were incorrect instead of doing `mob.loc == src`(which would fail if the mob was standing in the open cro) we now do `get_turf(mob) == get_turf(src)`(which works if the mob by anyway in on the turf of the machine) 3. **Code Improvements** - Get's rid of unused variables such as `sleep_factor`, `unconscious_factor` etc. Simply unused - Autodocs remaining variables and procs - Slightly touched on the UI shared by cryo, chem heater etc. Removes the `transfer_amounts` var that went unused in these cases   4. **Refactors** - Converted UI to typescript - Removes `check_nap_violations()` from `process()`. This proc checks if the occupant has an ID card & money in the account which makes no sense for cryo - Cryo will turn off if there is no occupant OR if there is no beaker - It no longer starts processing round start & only when the machine is turned on/off thus saving cpu - Removed old cryo icon states such `cell-open` , `cell-off` etc. These were used for the old cryo tube from years ago but are no longer needed now so the icon file size has been trimmed :cl: qol: adds more examines, screentips & balloon alerts for cryo actions, cryo can be pried open with a crowbar when there is no power to free someone trapped inside qol: cryo auto turns on immediately without a 2 second delay & without needing auto eject mode on fix: ejecting beaker from cryotube will put it in the players hand & not drop it on the floor fix: mobs & other stuff now render on top of cryo tubes and not bottom of it fix: cryo checks for if the mob is on the same turf as cryo is now fixed, i.e. you can no longer close the machine on yourself code: removed unused vars from cryo, autodocs vars, removed unused `transfer_amounts` from some chem machinery ui refactor: removed unused icon states for cryo tube, cryo no longer processes round start and it's UI is now typescript /:cl: * Chemical Reaction Tester/Debugger 2.0 (#80987) ## About The Pull Request This machine belongs in runtime station and is used for debugging reactions. This PR fully reworks the chemical reaction tester or `/obj/machinery/chem_recipe_debug` from ground up. Here is a comparison between the old and the new system **1. Old System** - Played all reactions sequentially and very rapidly, stopping the machine would halt all reactions. It has no ability to replay an reaction, Play forward/backward through the long reaction list or more importantly pick a reaction at random - It would spawn a ton of beakers per reaction and create a ton of reagent waste - It would spam the chat with a lot of `say()`, overloading us with too much information - Cannot debug soup reactions which require solid ingredients - Code quality is just poor & UI is clunky **2. New System** - Gives you the option to play 1 reaction at a time, You can play next/play previous/pick a reaction from the list. - Gives you the explicit option to create a beaker from the stored reagents or even better reuse a beaker if you inserted 1 in - No chat spams - Now has support for soup reactions - Reaction display now matches that of chem heater for easy understandability - Better code & typescript UI https://github.com/tgstation/tgstation/assets/110812394/d2a73913-b7b2-4e2a-903c-c43fec1923f5 ## Changelog :cl: refactor: chemical reaction tester in runtime station has been remastered from ground up. /:cl: --------- Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> --- .../components/unary_devices/cryo.dm | 569 ++++----- .../chemistry/machinery/chem_heater.dm | 75 +- .../chemistry/machinery/chem_recipe_debug.dm | 1023 +++++++++++------ icons/obj/medical/cryogenics.dmi | Bin 42361 -> 21449 bytes .../tgui/interfaces/ChemDispenser.tsx | 6 +- tgui/packages/tgui/interfaces/ChemHeater.tsx | 266 +++-- .../tgui/interfaces/ChemRecipeDebug.jsx | 326 ------ .../tgui/interfaces/ChemRecipeDebug.tsx | 437 +++++++ tgui/packages/tgui/interfaces/Cryo.jsx | 128 --- tgui/packages/tgui/interfaces/Cryo.tsx | 150 +++ .../tgui/interfaces/PortableChemMixer.tsx | 6 +- .../tgui/interfaces/common/BeakerContents.jsx | 19 - .../tgui/interfaces/common/BeakerDisplay.tsx | 1 - 13 files changed, 1728 insertions(+), 1278 deletions(-) delete mode 100644 tgui/packages/tgui/interfaces/ChemRecipeDebug.jsx create mode 100644 tgui/packages/tgui/interfaces/ChemRecipeDebug.tsx delete mode 100644 tgui/packages/tgui/interfaces/Cryo.jsx create mode 100644 tgui/packages/tgui/interfaces/Cryo.tsx delete mode 100644 tgui/packages/tgui/interfaces/common/BeakerContents.jsx diff --git a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm index ab3d53adacf..8cc2105052f 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm @@ -72,50 +72,37 @@ max_integrity = 350 armor_type = /datum/armor/unary_cryo_cell layer = MOB_LAYER - 0.2 //SKYRAT EDIT - Fixing the opacity of cryo cells - ORIGINAL: layer = MOB_LAYER - state_open = FALSE circuit = /obj/item/circuitboard/machine/cryo_tube occupant_typecache = list(/mob/living/carbon, /mob/living/simple_animal) processing_flags = NONE - + fair_market_price = 10 + payment_department = ACCOUNT_MED use_power = IDLE_POWER_USE idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.75 active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * 1.5 flags_1 = PREVENT_CLICK_UNDER_1 + ///If TRUE will eject the mob once healing is complete var/autoeject = TRUE - var/volume = 100 - + ///Increased via upgraded parts, higher values will provide better healing and use smaller cryoxodane var/efficiency = 1 - var/sleep_factor = 0.00125 - var/unconscious_factor = 0.001 - /// Our approximation of a mob's heat capacity. + ///Our approximation of a mob's heat capacity. Higher tier parts will provide better cooling for mobs var/heat_capacity = 20000 + ///Works with heat capacity and is increased with higher tier parts. How quickly the mobs temperature changes in the chamber var/conduction_coefficient = 0.3 - + ///The beaker usually contains cryoxadone that is pumped into the mob var/obj/item/reagent_containers/cup/beaker = null - var/consume_gas = FALSE - + ///Inform medical about cryo status over this radio var/obj/item/radio/radio - var/radio_key = /obj/item/encryptionkey/headset_med - var/radio_channel = RADIO_CHANNEL_MEDICAL - /// Visual content - Occupant var/atom/movable/visual/cryo_occupant/occupant_vis - - var/message_cooldown ///Cryo will continue to treat people with 0 damage but existing wounds, but will sound off when damage healing is done in case doctors want to directly treat the wounds instead var/treating_wounds = FALSE - /// Cryo should notify doctors if the patient is dead, and eject them if autoeject is enabled - var/patient_dead = FALSE - fair_market_price = 10 - payment_department = ACCOUNT_MED - /// Reference to the datum connector we're using to interface with the pipe network var/datum/gas_machine_connector/internal_connector /// Check if the machine has been turned on var/on = FALSE - /datum/armor/unary_cryo_cell energy = 100 fire = 30 @@ -123,75 +110,39 @@ /obj/machinery/cryo_cell/Initialize(mapload) . = ..() - if(is_operational) - begin_processing() radio = new(src) - radio.keyslot = new radio_key + radio.keyslot = new /obj/item/encryptionkey/headset_med radio.subspace_transmission = TRUE radio.canhear_range = 0 radio.recalculateChannels() - occupant_vis = new(null, src) + occupant_vis = new(mapload, src) vis_contents += occupant_vis - register_context() internal_connector = new(loc, src, dir, CELL_VOLUME * 0.5) -/obj/machinery/cryo_cell/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents) - . = ..() - if(same_z_layer) - return - SET_PLANE(occupant_vis, PLANE_TO_TRUE(occupant_vis.plane), new_turf) - -/obj/machinery/cryo_cell/set_occupant(atom/movable/new_occupant) - . = ..() - update_appearance() - -/obj/machinery/cryo_cell/on_construction(mob/user) - ..(user, dir, dir) - -/obj/machinery/cryo_cell/RefreshParts() - . = ..() - var/C - for(var/datum/stock_part/matter_bin/M in component_parts) - C += M.tier - - efficiency = initial(efficiency) * C - sleep_factor = initial(sleep_factor) * C - unconscious_factor = initial(unconscious_factor) * C - heat_capacity = initial(heat_capacity) / C - conduction_coefficient = initial(conduction_coefficient) * C - -/obj/machinery/cryo_cell/examine(mob/user) //this is leaving out everything but efficiency since they follow the same idea of "better beaker, better results" - . = ..() - if(in_range(user, src) || isobserver(user)) - . += span_notice("The status display reads: Efficiency at [efficiency*100]%.") - -/obj/machinery/cryo_cell/add_context(atom/source, list/context, obj/item/held_item, mob/user) - . = ..() - context[SCREENTIP_CONTEXT_CTRL_LMB] = "Turn [on ? "off" : "on"]" - context[SCREENTIP_CONTEXT_ALT_LMB] = "[state_open ? "Close" : "Open"] door" - if(!held_item) - return CONTEXTUAL_SCREENTIP_SET - switch(held_item.tool_behaviour) - if(TOOL_SCREWDRIVER) - context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel" - if(TOOL_WRENCH) - context[SCREENTIP_CONTEXT_LMB] = "Rotate" - return CONTEXTUAL_SCREENTIP_SET - + register_context() /obj/machinery/cryo_cell/Destroy() - vis_contents.Cut() + on = FALSE + vis_contents.Cut() QDEL_NULL(occupant_vis) QDEL_NULL(radio) QDEL_NULL(beaker) - QDEL_NULL(internal_connector) return ..() +/obj/machinery/cryo_cell/on_deconstruction() + if(occupant) + occupant.vis_flags &= ~VIS_INHERIT_PLANE + REMOVE_TRAIT(occupant, TRAIT_IMMOBILIZED, CRYO_TRAIT) + REMOVE_TRAIT(occupant, TRAIT_FORCED_STANDING, CRYO_TRAIT) + + if(beaker) + beaker.forceMove(drop_location()) + /obj/machinery/cryo_cell/contents_explosion(severity, target) . = ..() if(!beaker) @@ -205,28 +156,81 @@ if(EXPLODE_LIGHT) SSexplosions.low_mov_atom += beaker -/obj/machinery/cryo_cell/Exited(atom/movable/gone, direction) +/obj/machinery/cryo_cell/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents) . = ..() - if(gone == beaker) - beaker = null + if(same_z_layer) + return + SET_PLANE(occupant_vis, PLANE_TO_TRUE(occupant_vis.plane), new_turf) -/obj/machinery/cryo_cell/on_deconstruction() - if(occupant) - occupant.vis_flags &= ~VIS_INHERIT_PLANE - REMOVE_TRAIT(occupant, TRAIT_IMMOBILIZED, CRYO_TRAIT) - REMOVE_TRAIT(occupant, TRAIT_FORCED_STANDING, CRYO_TRAIT) +/obj/machinery/cryo_cell/set_occupant(atom/movable/new_occupant) + . = ..() + update_appearance() - if(beaker) - beaker.forceMove(drop_location()) - beaker = null +/obj/machinery/cryo_cell/RefreshParts() + . = ..() + var/C + for(var/datum/stock_part/matter_bin/M in component_parts) + C += M.tier -/obj/machinery/cryo_cell/update_icon_state() - icon_state = (state_open) ? "pod-open" : ((on && is_operational) ? "pod-on" : "pod-off") - return ..() + efficiency = initial(efficiency) * C + heat_capacity = initial(heat_capacity) / C + conduction_coefficient = initial(conduction_coefficient) * C + +/obj/machinery/cryo_cell/examine(mob/user) //this is leaving out everything but efficiency since they follow the same idea of "better beaker, better results" + . = ..() + if(in_range(user, src) || isobserver(user)) + . += span_notice("The status display reads: Efficiency at [efficiency * 100]%.") + if(occupant) + if(on) + . += span_notice("Someone's inside [src]!") + else + . += span_notice("You can barely make out a form floating in [src].") + else + . += span_notice("[src] seems empty.") + if(beaker) + . += span_notice("A beaker of [beaker.reagents.maximum_volume]u capacity is located inside.") + else + . += span_warning("Its missing a beaker.") + + . += span_notice("Use [EXAMINE_HINT("Alt-Click")] to [state_open ? "Close" : "Open"] the machine.") + . += span_notice("Use [EXAMINE_HINT("Ctrl-Click")] to turn [on ? "Off" : "On"] the machine.") + + . += span_notice("Its maintainence panel can be [EXAMINE_HINT("screwed")] open.") + if(panel_open) + . += span_notice("[src] can be [EXAMINE_HINT("pried")] apart.") + . += span_notice("[src] can be rotated with a [EXAMINE_HINT("wrench")].") + else if(machine_stat & NOPOWER) + . += span_notice("[src] can be [EXAMINE_HINT("pried")] open.") + +/obj/machinery/cryo_cell/add_context(atom/source, list/context, obj/item/held_item, mob/user) + context[SCREENTIP_CONTEXT_CTRL_LMB] = "Turn [on ? "off" : "on"]" + context[SCREENTIP_CONTEXT_ALT_LMB] = "[state_open ? "Close" : "Open"] door" + if(isnull(held_item)) + return CONTEXTUAL_SCREENTIP_SET + + if(QDELETED(beaker) && istype(held_item, /obj/item/reagent_containers/cup)) + context[SCREENTIP_CONTEXT_LMB] = "Insert beaker" + return CONTEXTUAL_SCREENTIP_SET + + switch(held_item.tool_behaviour) + if(TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel" + if(TOOL_CROWBAR) + if(!state_open && !panel_open && !is_operational) + context[SCREENTIP_CONTEXT_LMB] = "Pry Open" + else if(panel_open) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + if(TOOL_WRENCH) + context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Rotate" : ""]" + return CONTEXTUAL_SCREENTIP_SET /obj/machinery/cryo_cell/update_icon() - . = ..() SET_PLANE_IMPLICIT(src, initial(plane)) + return ..() + +/obj/machinery/cryo_cell/update_icon_state() + icon_state = state_open ? "pod-open" : ((on && is_operational) ? "pod-on" : "pod-off") + return ..() /obj/machinery/cryo_cell/update_overlays() . = ..() @@ -234,117 +238,133 @@ . += "pod-panel" if(state_open) return - if(on && is_operational) - . += mutable_appearance('icons/obj/medical/cryogenics.dmi', "cover-on", ABOVE_ALL_MOB_LAYER, src, plane = ABOVE_GAME_PLANE) - else - . += mutable_appearance('icons/obj/medical/cryogenics.dmi', "cover-off", ABOVE_ALL_MOB_LAYER, src, plane = ABOVE_GAME_PLANE) + . += mutable_appearance('icons/obj/medical/cryogenics.dmi', "cover-[on && is_operational ? "on" : "off"]", ABOVE_ALL_MOB_LAYER, src, plane = ABOVE_GAME_PLANE) -/obj/machinery/cryo_cell/nap_violation(mob/violator) - open_machine() +/obj/machinery/cryo_cell/dump_inventory_contents(list/subset = list(occupant)) + //only drop the mob and nothing else by default when opening the machine + return ..(subset) +/obj/machinery/cryo_cell/Exited(atom/movable/gone, direction) + . = ..() + if(gone == beaker) + beaker = null +/** + * Turns the machine on/off + * + * Arguments + * * active - TRUE to turn the machine on, FALSE to turn it off + */ /obj/machinery/cryo_cell/proc/set_on(active) + PRIVATE_PROC(TRUE) + if(on == active) return + SEND_SIGNAL(src, COMSIG_CRYO_SET_ON, active) . = on on = active - if(on) - update_use_power(ACTIVE_POWER_USE) - else - update_use_power(IDLE_POWER_USE) update_appearance() -/obj/machinery/cryo_cell/on_set_is_operational(old_value) - if(old_value) //Turned off - set_on(FALSE) - end_processing() - else //Turned on + update_use_power(on ? ACTIVE_POWER_USE : IDLE_POWER_USE) + if(on) //Turned on begin_processing() + else //Turned off + end_processing() +/obj/machinery/cryo_cell/begin_processing() + . = ..() + SSair.start_processing_machine(src) + +/obj/machinery/cryo_cell/end_processing() + . = ..() + SSair.stop_processing_machine(src) + +/obj/machinery/cryo_cell/on_set_is_operational(old_value) + //Turned off + if(old_value) + set_on(FALSE) /obj/machinery/cryo_cell/process(seconds_per_tick) - ..() - if(!occupant) - return + if(!on || QDELETED(occupant)) + //somehow an deleting mob is inside us. dump everything out + if(!isnull(occupant) && QDELING(occupant)) + open_machine() + on = FALSE //in case panel was open we need to set to FALSE explicitly - if(!on) - // Should turn on if set to auto - if(autoeject) - set_on(TRUE) - else - return + //if not on end processing + if(!on) + set_on(FALSE) //this explicitly disables processing so is nessassary + . = PROCESS_KILL + + return var/mob/living/mob_occupant = occupant if(mob_occupant.on_fire) mob_occupant.extinguish_mob() - if(!check_nap_violations()) - return if(mob_occupant.stat == DEAD) // Notify doctors and potentially eject if the patient is dead set_on(FALSE) var/msg = "Patient is deceased." if(autoeject) // Eject if configured. msg += " Auto ejecting patient now." open_machine() - // Only need to tell them once - if(!patient_dead) - playsound(src, 'sound/machines/cryo_warning.ogg', volume) - patient_dead = TRUE - radio.talk_into(src, msg, radio_channel) - return + playsound(src, 'sound/machines/cryo_warning.ogg', 100) + radio.talk_into(src, msg, RADIO_CHANNEL_MEDICAL) + return PROCESS_KILL - patient_dead = FALSE - - if(mob_occupant.get_organic_health() >= mob_occupant.getMaxHealth()) // Don't bother with fully healed people. + // Don't bother with fully healed people. + if(mob_occupant.get_organic_health() >= mob_occupant.getMaxHealth()) if(iscarbon(mob_occupant)) var/mob/living/carbon/C = mob_occupant if(C.all_wounds) if(!treating_wounds) // if we have wounds and haven't already alerted the doctors we're only dealing with the wounds, let them know treating_wounds = TRUE - playsound(src, 'sound/machines/cryo_warning.ogg', volume) // Bug the doctors. - var/msg = "Patient vitals fully recovered, continuing automated wound treatment." - radio.talk_into(src, msg, radio_channel) + playsound(src, 'sound/machines/cryo_warning.ogg', 100) // Bug the doctors. + radio.talk_into(src, "Patient vitals fully recovered, continuing automated wound treatment.", RADIO_CHANNEL_MEDICAL) else // otherwise if we were only treating wounds and now we don't have any, turn off treating_wounds so we can boot 'em out treating_wounds = FALSE if(!treating_wounds) set_on(FALSE) - playsound(src, 'sound/machines/cryo_warning.ogg', volume) // Bug the doctors. + playsound(src, 'sound/machines/cryo_warning.ogg', 100) // Bug the doctors. var/msg = "Patient fully restored." if(autoeject) // Eject if configured. msg += " Auto ejecting patient now." open_machine() - radio.talk_into(src, msg, radio_channel) - return + radio.talk_into(src, msg, RADIO_CHANNEL_MEDICAL) + return PROCESS_KILL var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] - - if(air1.total_moles() > CRYO_MIN_GAS_MOLES) - if(beaker) - beaker.reagents.trans_to( - occupant, - (CRYO_TX_QTY / (efficiency * CRYO_MULTIPLY_FACTOR)) * seconds_per_tick, - efficiency * CRYO_MULTIPLY_FACTOR, - methods = VAPOR - ) - consume_gas = TRUE - return TRUE + if(!QDELETED(beaker) && air1.total_moles() > CRYO_MIN_GAS_MOLES) + beaker.reagents.trans_to( + occupant, + (CRYO_TX_QTY / (efficiency * CRYO_MULTIPLY_FACTOR)) * seconds_per_tick, + efficiency * CRYO_MULTIPLY_FACTOR, + methods = VAPOR + ) /obj/machinery/cryo_cell/process_atmos() - ..() - if(!on) - return + return PROCESS_KILL var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] - if(!internal_connector.gas_connector.nodes[1] || !internal_connector.gas_connector.airs[1] || !air1.gases.len || air1.total_moles() < CRYO_MIN_GAS_MOLES) // Turn off if the machine won't work. - var/msg = "Insufficient cryogenic gas, shutting down." - radio.talk_into(src, msg, radio_channel) + //check for workable conditions + if(!internal_connector.gas_connector.nodes[1] || !air1 || !air1.gases.len || air1.total_moles() < CRYO_MIN_GAS_MOLES) // Turn off if the machine won't work. set_on(FALSE) - return + var/msg = "Insufficient cryogenic gas, shutting down." + if(autoeject) // Eject if configured. + msg += " Auto ejecting patient now." + open_machine() + radio.talk_into(src, msg, RADIO_CHANNEL_MEDICAL) + return PROCESS_KILL - if(occupant) + //take damage from high temperatures + if(air1.temperature > 2000) + take_damage(clamp((air1.temperature) / 200, 10, 20), BURN) + + //adjust temperature of mob + if(!QDELETED(occupant)) var/mob/living/mob_occupant = occupant var/cold_protection = 0 var/temperature_delta = air1.temperature - mob_occupant.bodytemperature // The only semi-realistic thing here: share temperature between the cell and the occupant. @@ -355,7 +375,6 @@ if(abs(temperature_delta) > 1) var/air_heat_capacity = air1.heat_capacity() - var/heat = ((1 - cold_protection) * 0.1 + conduction_coefficient) * CALCULATE_CONDUCTION_ENERGY(temperature_delta, heat_capacity, air_heat_capacity) mob_occupant.adjust_bodytemperature(heat / heat_capacity, TCMB) @@ -366,48 +385,42 @@ var/mob/living/carbon/human/humi = mob_occupant humi.adjust_coretemperature(humi.bodytemperature - humi.coretemperature) - - air1.garbage_collect() - - if(air1.temperature > 2000) - take_damage(clamp((air1.temperature)/200, 10, 20), BURN) - - internal_connector.gas_connector.update_parents() + //spread temperature changes throughout the pipenet + internal_connector.gas_connector.update_parents() /obj/machinery/cryo_cell/handle_internal_lifeform(mob/lifeform_inside_me, breath_request) - if(breath_request <= 0) return null + + //return breathable air var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] var/breath_percentage = breath_request / air1.volume - return air1.remove(air1.total_moles() * breath_percentage) + . = air1.remove(air1.total_moles() * breath_percentage) + + //update molar changes throughout the pipenet + internal_connector.gas_connector.update_parents() /obj/machinery/cryo_cell/assume_air(datum/gas_mixture/giver) internal_connector.gas_connector.airs[1].merge(giver) -/obj/machinery/cryo_cell/relaymove(mob/living/user, direction) - if(message_cooldown <= world.time) - message_cooldown = world.time + 50 - to_chat(user, span_warning("[src]'s door won't budge!")) +/obj/machinery/cryo_cell/return_temperature() + var/datum/gas_mixture/internal_air = internal_connector.gas_connector.airs[1] -/obj/machinery/cryo_cell/open_machine(drop = FALSE, density_to_set = FALSE) + return internal_air.total_moles() > CRYO_MIN_GAS_MOLES ? internal_air.temperature : ..() + +/obj/machinery/cryo_cell/open_machine(drop = TRUE, density_to_set = FALSE) if(!state_open && !panel_open) set_on(FALSE) - for(var/mob/M in contents) //only drop mobs - M.forceMove(get_turf(src)) - set_occupant(null) flick("pod-open-anim", src) return ..() /obj/machinery/cryo_cell/close_machine(mob/living/carbon/user, density_to_set = TRUE) treating_wounds = FALSE - if((isnull(user) || istype(user)) && state_open && !panel_open) - if(loc == user?.loc) - to_chat(user, span_warning("You can't close [src] on yourself!")) - return + if(state_open && !panel_open) flick("pod-close-anim", src) - ..(user) - return occupant + . = ..() + if(!QDELETED(occupant)) //auto on if an occupant is inside + set_on(TRUE) /obj/machinery/cryo_cell/container_resist_act(mob/living/user) user.changeNext_move(CLICK_CD_BREAKOUT) @@ -422,16 +435,6 @@ span_notice("You successfully break out of [src]!")) open_machine() -/obj/machinery/cryo_cell/examine(mob/user) - . = ..() - if(occupant) - if(on) - . += "Someone's inside [src]!" - else - . += "You can barely make out a form floating in [src]." - else - . += "[src] seems empty." - /obj/machinery/cryo_cell/MouseDrop_T(mob/target, mob/user) if(user.incapacitated() || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !ISADVANCEDTOOLUSER(user)) return @@ -445,25 +448,32 @@ close_machine(target) /obj/machinery/cryo_cell/screwdriver_act(mob/living/user, obj/item/tool) + . = ITEM_INTERACT_BLOCKING + if(on) + balloon_alert(user, "turn off!") + return + if(occupant) + balloon_alert(user, "occupant inside!") + return - if(!on && !occupant && !state_open && (default_deconstruction_screwdriver(user, "pod-off", "pod-off", tool))) + if(default_deconstruction_screwdriver(user, "pod-off", "pod-off", tool)) update_appearance() - else - to_chat(user, "You can't access the maintenance panel while the pod is " \ - + (on ? "active" : (occupant ? "full" : "open")) + "!") - return ITEM_INTERACT_SUCCESS - -/obj/machinery/cryo_cell/crowbar_act(mob/living/user, obj/item/tool) - if(on || state_open) - return FALSE - if(!panel_open) - balloon_alert(user, "open panel!") return ITEM_INTERACT_SUCCESS - var/unsafe_wrenching = FALSE - var/filled_pipe = FALSE - var/datum/gas_mixture/environment_air = loc.return_air() - var/datum/gas_mixture/inside_air = internal_connector.gas_connector.airs[1] +/obj/machinery/cryo_cell/crowbar_act(mob/living/user, obj/item/tool) + . = ITEM_INTERACT_BLOCKING + if(on) + balloon_alert(user, "turn off!") + return + + var/can_crowbar = FALSE + if(!state_open && !panel_open && !is_operational) //can pry open + can_crowbar = TRUE + else if(panel_open) //can deconstruct + can_crowbar = TRUE + if(!can_crowbar) + return + var/obj/machinery/atmospherics/node = internal_connector.gas_connector.nodes[1] var/internal_pressure = 0 @@ -471,165 +481,180 @@ var/obj/machinery/atmospherics/components/unary/portables_connector/portable_devices_connector = node internal_pressure = !portable_devices_connector.connected_device ? 1 : 0 + var/datum/gas_mixture/inside_air = internal_connector.gas_connector.airs[1] if(inside_air.total_moles() > 0) - filled_pipe = TRUE if(!node || internal_pressure > 0) + var/datum/gas_mixture/environment_air = loc.return_air() internal_pressure = inside_air.return_pressure() - environment_air.return_pressure() - if(!filled_pipe) - default_deconstruction_crowbar(tool) - return ITEM_INTERACT_SUCCESS - - to_chat(user, span_notice("You begin to unfasten \the [src]...")) - + var/unsafe_release = FALSE if(internal_pressure > 2 * ONE_ATMOSPHERE) - to_chat(user, span_warning("As you begin deconstructing \the [src] a gush of air blows in your face... maybe you should reconsider?")) - unsafe_wrenching = TRUE + to_chat(user, span_warning("As you begin prying \the [src] a gush of air blows in your face... maybe you should reconsider?")) + if(!do_after(user, 2 SECONDS, target = src)) + return + unsafe_release = TRUE - if(!do_after(user, 2 SECONDS, src)) - return - if(unsafe_wrenching) + var/deconstruct = FALSE + if(!default_pry_open(tool)) + if(!default_deconstruction_crowbar(tool, custom_deconstruct = TRUE)) + return + else + deconstruct = TRUE + + if(unsafe_release) internal_connector.gas_connector.unsafe_pressure_release(user, internal_pressure) tool.play_tool_sound(src, 50) - deconstruct(TRUE) + if(deconstruct) + deconstruct(TRUE) return ITEM_INTERACT_SUCCESS /obj/machinery/cryo_cell/wrench_act(mob/living/user, obj/item/tool) - if(on || occupant || state_open) - return FALSE + . = ITEM_INTERACT_BLOCKING + if(on) + balloon_alert(user, "turn off!") + return + if(occupant) + balloon_alert(user, "occupant inside!") + return + if(state_open) + balloon_alert(user, "close first!") + return + if(default_change_direction_wrench(user, tool)) update_appearance() return ITEM_INTERACT_SUCCESS /obj/machinery/cryo_cell/attackby(obj/item/I, mob/user, params) if(istype(I, /obj/item/reagent_containers/cup)) - . = 1 //no afterattack + . = TRUE //no afterattack if(beaker) - to_chat(user, span_warning("A beaker is already loaded into [src]!")) + balloon_alert(user, "beaker present!") return if(!user.transferItemToLoc(I, src)) return beaker = I - user.visible_message(span_notice("[user] places [I] in [src]."), \ - span_notice("You place [I] in [src].")) + balloon_alert(user, "beaker inserted") var/reagentlist = pretty_string_from_reagent_list(I.reagents.reagent_list) user.log_message("added an [I] to cryo containing [reagentlist].", LOG_GAME) return + return ..() /obj/machinery/cryo_cell/ui_state(mob/user) return GLOB.notcontained_state - /obj/machinery/cryo_cell/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "Cryo", name) ui.open() +/obj/machinery/cryo_cell/ui_static_data(mob/user) + . = list() + .["T0C"] = T0C + /obj/machinery/cryo_cell/ui_data() - var/list/data = list() - data["isOperating"] = on - data["hasOccupant"] = occupant ? TRUE : FALSE - data["isOpen"] = state_open - data["autoEject"] = autoeject + . = list() + .["isOperating"] = on + .["isOpen"] = state_open + .["autoEject"] = autoeject - data["occupant"] = list() - if(occupant) + var/list/occupant_data = null + if(!QDELETED(occupant)) var/mob/living/mob_occupant = occupant - data["occupant"]["name"] = mob_occupant.name + occupant_data = list() + + occupant_data["name"] = mob_occupant.name if(mob_occupant.stat == DEAD) - data["occupant"]["stat"] = "Dead" - data["occupant"]["statstate"] = "bad" + occupant_data["stat"] = "Dead" else if (HAS_TRAIT(mob_occupant, TRAIT_KNOCKEDOUT)) - data["occupant"]["stat"] = "Unconscious" - data["occupant"]["statstate"] = "good" + occupant_data["stat"] = "Unconscious" else - data["occupant"]["stat"] = "Conscious" - data["occupant"]["statstate"] = "bad" + occupant_data["stat"] = "Conscious" - data["occupant"]["bodyTemperature"] = round(mob_occupant.bodytemperature, 1) - if(mob_occupant.bodytemperature < T0C) // Green if the mob can actually be healed by cryoxadone. - data["occupant"]["temperaturestatus"] = "good" - else - data["occupant"]["temperaturestatus"] = "bad" + occupant_data["bodyTemperature"] = round(mob_occupant.bodytemperature, 1) - data["occupant"]["health"] = round(mob_occupant.health, 1) - data["occupant"]["maxHealth"] = mob_occupant.maxHealth - data["occupant"]["minHealth"] = HEALTH_THRESHOLD_DEAD - data["occupant"]["bruteLoss"] = round(mob_occupant.getBruteLoss(), 1) - data["occupant"]["oxyLoss"] = round(mob_occupant.getOxyLoss(), 1) - data["occupant"]["toxLoss"] = round(mob_occupant.getToxLoss(), 1) - data["occupant"]["fireLoss"] = round(mob_occupant.getFireLoss(), 1) + occupant_data["health"] = mob_occupant.health + occupant_data["maxHealth"] = mob_occupant.maxHealth + occupant_data["bruteLoss"] = mob_occupant.getBruteLoss() + occupant_data["oxyLoss"] = mob_occupant.getOxyLoss() + occupant_data["toxLoss"] = mob_occupant.getToxLoss() + occupant_data["fireLoss"] = mob_occupant.getFireLoss() + .["occupant"] = occupant_data var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] - data["cellTemperature"] = round(air1.temperature, 1) + .["cellTemperature"] = air1.temperature - data["isBeakerLoaded"] = beaker ? TRUE : FALSE - var/beakerContents = list() - if(beaker && beaker.reagents && beaker.reagents.reagent_list.len) - for(var/datum/reagent/R in beaker.reagents.reagent_list) - beakerContents += list(list("name" = R.name, "volume" = R.volume)) - data["beakerContents"] = beakerContents - return data + var/list/beaker_data = null + if(!QDELETED(beaker)) + beaker_data = list() + beaker_data["maxVolume"] = beaker.volume + beaker_data["currentVolume"] = round(beaker.reagents.total_volume, 0.01) + var/list/beakerContents = list() + if(length(beaker.reagents.reagent_list)) + for(var/datum/reagent/reagent in beaker.reagents.reagent_list) + beakerContents += list(list("name" = reagent.name, "volume" = round(reagent.volume, 0.01))) // list in a list because Byond merges the first list... + beaker_data["contents"] = beakerContents + .["beaker"] = beaker_data -/obj/machinery/cryo_cell/ui_act(action, params) +/obj/machinery/cryo_cell/ui_act(action, params, datum/tgui/ui, datum/ui_state/state) . = ..() if(.) return + switch(action) if("power") if(on) set_on(FALSE) else if(!state_open) set_on(TRUE) - . = TRUE + return TRUE + if("door") - if(state_open) + if(state_open && get_turf(ui.user) != get_turf(src)) close_machine() else open_machine() - . = TRUE + return TRUE + if("autoeject") autoeject = !autoeject - . = TRUE - if("ejectbeaker") - if(beaker) - beaker.forceMove(drop_location()) - if(Adjacent(usr) && !issilicon(usr)) - usr.put_in_hands(beaker) - beaker = null - . = TRUE + return TRUE + + if("eject") + if(!QDELETED(beaker)) + var/mob/living/user = ui.user + if(Adjacent(user) && !issilicon(user)) + user.put_in_hands(beaker) + else + beaker.forceMove(drop_location()) + return TRUE /obj/machinery/cryo_cell/can_interact(mob/user) - return ..() && user.loc != src + //must not be in the machine or on its turf to interact + if(get_turf(user) == get_turf(src)) + return FALSE + return ..() /obj/machinery/cryo_cell/CtrlClick(mob/user) if(can_interact(user) && !state_open) - if(set_on(!on)) - balloon_alert(user, "turned [on ? "on" : "off"]") + set_on(!on) + balloon_alert(user, "turned [on ? "on" : "off"]") return ..() /obj/machinery/cryo_cell/AltClick(mob/user) if(can_interact(user)) - balloon_alert(user, "[state_open ? "closing" : "opening"] door") if(state_open) close_machine() else open_machine() + balloon_alert(user, "door [state_open ? "opened" : "closed"]") return ..() /obj/machinery/cryo_cell/get_remote_view_fullscreens(mob/user) user.overlay_fullscreen("remote_view", /atom/movable/screen/fullscreen/impaired, 1) -/obj/machinery/cryo_cell/return_temperature() - var/datum/gas_mixture/G = internal_connector.gas_connector.airs[1] - - if(G.total_moles() > 10) - return G.temperature - return ..() - #undef MAX_TEMPERATURE #undef CRYO_MULTIPLY_FACTOR #undef CRYO_TX_QTY diff --git a/code/modules/reagents/chemistry/machinery/chem_heater.dm b/code/modules/reagents/chemistry/machinery/chem_heater.dm index b5a43adb4cb..6c036d37bef 100644 --- a/code/modules/reagents/chemistry/machinery/chem_heater.dm +++ b/code/modules/reagents/chemistry/machinery/chem_heater.dm @@ -34,12 +34,31 @@ QDEL_NULL(beaker) return ..() -/obj/machinery/chem_heater/Exited(atom/movable/gone, direction) - . = ..() - if(gone == beaker) - UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP) - beaker = null - update_appearance() + +/obj/machinery/chem_heater/add_context(atom/source, list/context, obj/item/held_item, mob/user) + if(isnull(held_item) || (held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1)) + return NONE + + if(!QDELETED(beaker)) + if(istype(held_item, /obj/item/reagent_containers/dropper) || istype(held_item, /obj/item/reagent_containers/syringe)) + context[SCREENTIP_CONTEXT_LMB] = "Inject" + return CONTEXTUAL_SCREENTIP_SET + if(is_reagent_container(held_item) && held_item.is_open_container()) + context[SCREENTIP_CONTEXT_LMB] = "Replace beaker" + return CONTEXTUAL_SCREENTIP_SET + else if(is_reagent_container(held_item) && held_item.is_open_container()) + context[SCREENTIP_CONTEXT_LMB] = "Insert beaker" + return CONTEXTUAL_SCREENTIP_SET + + if(held_item.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "Open panel" + return CONTEXTUAL_SCREENTIP_SET + else if(panel_open && held_item.tool_behaviour == TOOL_CROWBAR) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + return CONTEXTUAL_SCREENTIP_SET + + return NONE + /obj/machinery/chem_heater/update_icon_state() icon_state = "[base_icon_state][beaker ? 1 : 0]b" @@ -54,6 +73,13 @@ replace_beaker(user) return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN +/obj/machinery/chem_heater/Exited(atom/movable/gone, direction) + . = ..() + if(gone == beaker) + UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP) + beaker = null + update_appearance() + /obj/machinery/chem_heater/attack_robot_secondary(mob/user, list/modifiers) return attack_hand_secondary(user, modifiers) @@ -116,30 +142,6 @@ for(var/datum/tgui/ui in src.open_uis) ui.send_update() -/obj/machinery/chem_heater/add_context(atom/source, list/context, obj/item/held_item, mob/user) - if(isnull(held_item) || (held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1)) - return NONE - - if(!QDELETED(beaker)) - if(istype(held_item, /obj/item/reagent_containers/dropper) || istype(held_item, /obj/item/reagent_containers/syringe)) - context[SCREENTIP_CONTEXT_LMB] = "Inject" - return CONTEXTUAL_SCREENTIP_SET - if(is_reagent_container(held_item) && held_item.is_open_container()) - context[SCREENTIP_CONTEXT_LMB] = "Replace beaker" - return CONTEXTUAL_SCREENTIP_SET - else if(is_reagent_container(held_item) && held_item.is_open_container()) - context[SCREENTIP_CONTEXT_LMB] = "Insert beaker" - return CONTEXTUAL_SCREENTIP_SET - - if(held_item.tool_behaviour == TOOL_SCREWDRIVER) - context[SCREENTIP_CONTEXT_LMB] = "Open panel" - return CONTEXTUAL_SCREENTIP_SET - else if(panel_open && held_item.tool_behaviour == TOOL_CROWBAR) - context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" - return CONTEXTUAL_SCREENTIP_SET - - return NONE - /obj/machinery/chem_heater/examine(mob/user) . = ..() if(in_range(user, src) || isobserver(user)) @@ -220,13 +222,12 @@ if(!QDELETED(beaker)) beaker_data = list() beaker_data["maxVolume"] = beaker.volume - beaker_data["transferAmounts"] = beaker.possible_transfer_amounts beaker_data["pH"] = round(beaker.reagents.ph, 0.01) - beaker_data["currentVolume"] = round(beaker.reagents.total_volume, 0.01) + beaker_data["currentVolume"] = round(beaker.reagents.total_volume, CHEMICAL_VOLUME_ROUNDING) var/list/beakerContents = list() if(length(beaker.reagents.reagent_list)) for(var/datum/reagent/reagent in beaker.reagents.reagent_list) - beakerContents += list(list("name" = reagent.name, "volume" = round(reagent.volume, 0.01))) // list in a list because Byond merges the first list... + beakerContents += list(list("name" = reagent.name, "volume" = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING))) // list in a list because Byond merges the first list... beaker_data["contents"] = beakerContents chem_temp = beaker.reagents.chem_temp .["beaker"] = beaker_data @@ -234,15 +235,13 @@ var/list/active_reactions = list() var/flashing = DISABLE_FLASHING //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false - for(var/_reaction in beaker?.reagents.reaction_list) - var/datum/equilibrium/equilibrium = _reaction + for(var/datum/equilibrium/equilibrium as anything in beaker?.reagents.reaction_list) if(!equilibrium.reaction.results)//Incase of no result reactions continue - var/_reagent = equilibrium.reaction.results[1] - var/datum/reagent/reagent = beaker?.reagents.has_reagent(_reagent) //Reactions are named after their primary products + var/datum/reagents/beaker_reagents = beaker.reagents + var/datum/reagent/reagent = beaker_reagents.has_reagent(equilibrium.reaction.results[1]) //Reactions are named after their primary products if(!reagent) continue - var/datum/reagents/beaker_reagents = beaker.reagents //check for danger levels primirarly overheating var/overheat = FALSE diff --git a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm index b5f2d31dfa5..9d81188fb40 100644 --- a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm +++ b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm @@ -2,394 +2,240 @@ * A debug chem tester that will process through all recipies automatically and try to react them. * Highlights low purity reactions and and reactions that don't happen */ + +///don't alter the temperatrue of the reaction +#define USE_REACTION_TEMPERATURE 0 +///force a user specified value for temperature on the reaction +#define USE_USER_TEMPERATURE 1 +///force the minimum required temperature for the reaction to start on the reaction +#define USE_MINIMUM_TEMPERATURE 2 +///force the optimal temperature for the reaction +#define USE_OPTIMAL_TEMPERATURE 3 +///force the overheat temperature for the reaction. At this point reagents start to decrease +#define USE_OVERHEAT_TEMPERATURE 4 + +///Play the next reaction i.e. increment current_reaction_index +#define PLAY_NEXT_REACTION 0 +///Play the previous reaction i.e. decrement current_reaction_index +#define PLAY_PREVIOUS_REACTION 1 +///Pick a reaction at random i.e. user decides via input list what the value of current_reaction_index should be +#define PLAY_USER_REACTION 2 + +///Maximum volume of reagents this machine & its required container can hold +#define MAXIMUM_HOLDER_VOLUME 9000 + /obj/machinery/chem_recipe_debug name = "chemical reaction tester" - density = TRUE icon = 'icons/obj/medical/chemical.dmi' icon_state = "HPLC_debug" + density = TRUE idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.4 resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE - ///List of every reaction in the game kept locally for easy access - var/list/cached_reactions = list() - ///What index in the cached_reactions we're in - var/index = 1 - ///If the machine is currently processing through the list - var/processing = FALSE - ///Final output that highlights all of the reactions with inoptimal purity/voolume at base - var/problem_string - ///Final output that highlights all of the reactions with inoptimal purity/voolume at base - var/impure_string - ///The count of reactions that resolve between 1 - 0.9 purity - var/minorImpurity - ///The count of reactions that resolve below 0.9 purity - var/majorImpurity - ///If we failed to react this current chem so use a lower temp - all reactions only - var/failed = 0 - ///If we're forcing optimal conditions - var/should_force_temp = FALSE - var/should_force_ph = FALSE - ///Forced values - var/force_temp = 300 - var/force_ph = 7 - ///Multiplier of product - var/vol_multiplier = 20 - ///If we're reacting - var/react = FALSE - ///Number of delta times taken to react - var/react_time = 0 - ///IF we're doing EVERY reaction - var/process_all = FALSE - ///The name - var/list/reaction_names = list() - ///If it's started - var/reaction_stated = FALSE - ///If we spawn a beaker at the end of a reaction or not - var/beaker_spawn = FALSE - ///If we force min temp on reaction setup - var/min_temp = FALSE - ///The recipe we're editing - var/datum/chemical_reaction/edit_recipe -///Create reagents datum + ///Temperature to be imposed on the reaction + var/forced_temp = DEFAULT_REAGENT_TEMPERATURE + ///The mode for setting reaction temps. see temp defines + var/temp_mode = USE_REACTION_TEMPERATURE + ///The ph to be imposed on the reaction + var/forced_ph = CHEMICAL_NORMAL_PH + ///if TRUE will use forced_ph else don't alter the ph of the reaction + var/use_forced_ph = FALSE + ///The purity of all reagents to be imposed on the reaction + var/forced_purity = 1.0 + ///If TRUE will use forced_purity else don't alter the purity of the reaction + var/use_forced_purity = FALSE + ///The multiplier to be applied on the selected reaction required reagents to start the reaction + var/volume_multiplier = 1 + + ///Cached copy all reactions mapped with their name + var/static/list/all_reaction_list + ///The list of reactions to test + var/list/datum/chemical_reaction/reactions_to_test = list() + ///The index in reactions_to_test list which points to the current reaction under test + var/current_reaction_index = 0 + ///Decides which reaction to play in the reactions_to_test list see Play defines + var/current_reaction_mode = PLAY_NEXT_REACTION + ///The current reaction we are editing + var/datum/chemical_reaction/edit_reaction + ///The current var of the reaction we are editing + var/edit_var = "Required Temp" + + ///The target reagents to we are working with. can vary if an reaction requires a specific container + var/datum/reagents/target_reagents + ///The beaker inside this machine, if null will create a new one + var/obj/item/reagent_containers/cup/beaker/bluespace/beaker + ///The default reagent container required for the selected test reaction if any + var/obj/item/reagent_containers/required_container + /obj/machinery/chem_recipe_debug/Initialize(mapload) . = ..() - create_reagents(9000)//I want to make sure everything fits - end_processing() -///Enable the machine -/obj/machinery/chem_recipe_debug/attackby(obj/item/I, mob/user, params) + create_reagents(MAXIMUM_HOLDER_VOLUME) + target_reagents = reagents + RegisterSignal(reagents, COMSIG_REAGENTS_REACTION_STEP, TYPE_PROC_REF(/obj/machinery/chem_recipe_debug, on_reaction_step)) + register_context() + + if(isnull(all_reaction_list)) + all_reaction_list = list() + for(var/datum/reagent/reagent as anything in GLOB.chemical_reactions_list_reactant_index) + for(var/datum/chemical_reaction/reaction as anything in GLOB.chemical_reactions_list_reactant_index[reagent]) + all_reaction_list[extract_reaction_name(reaction)] = reaction + +/obj/machinery/chem_recipe_debug/Destroy() + reactions_to_test.Cut() + target_reagents = null + edit_reaction = null + QDEL_NULL(beaker) + QDEL_NULL(required_container) + UnregisterSignal(reagents, COMSIG_REAGENTS_REACTION_STEP) . = ..() - ui_interact(usr) -///Enable the machine -/obj/machinery/chem_recipe_debug/AltClick(mob/living/user) +/obj/machinery/chem_recipe_debug/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() - if(processing) - say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]") - return - process_all = TRUE - say("Starting processing") - setup_reactions() - begin_processing() + if(isnull(held_item) || (held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1)) + return NONE -///Resets the index, and creates the cached_reaction list from all possible reactions -/obj/machinery/chem_recipe_debug/proc/setup_reactions() - cached_reactions = list() - if(process_all) - for(var/reaction in GLOB.chemical_reactions_list_reactant_index) - if(is_type_in_list(GLOB.chemical_reactions_list_reactant_index[reaction], cached_reactions)) - continue - cached_reactions += GLOB.chemical_reactions_list_reactant_index[reaction] + if(!QDELETED(beaker)) + if(is_reagent_container(held_item) && held_item.is_open_container()) + context[SCREENTIP_CONTEXT_LMB] = "Replace beaker" + return CONTEXTUAL_SCREENTIP_SET + else if(is_reagent_container(held_item) && held_item.is_open_container()) + context[SCREENTIP_CONTEXT_LMB] = "Insert beaker" + return CONTEXTUAL_SCREENTIP_SET + +/obj/machinery/chem_recipe_debug/examine(mob/user) + . = ..() + if(!QDELETED(beaker)) + . += span_notice("A beaker of [beaker.reagents.maximum_volume]u capacity is inside.") else - cached_reactions = reaction_names - reagents.clear_reagents() - index = 1 - processing = TRUE + . += span_notice("No beaker is present. A new will be created when ejecting.") + +/obj/machinery/chem_recipe_debug/Exited(atom/movable/gone, direction) + . = ..() + if(gone == beaker) + beaker = null + +/obj/machinery/chem_recipe_debug/attackby(obj/item/held_item, mob/user, params) + if((held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1)) + return ..() + + if(is_reagent_container(held_item) && held_item.is_open_container()) + . = TRUE + if(!QDELETED(beaker)) + try_put_in_hand(beaker, user) + if(!user.transferItemToLoc(held_item, src)) + return + beaker = held_item + +/** + * Extracts a human readable name for this chemical reaction + * Arguments + * + * * datum/chemical_reaction/reaction - the reaction who's name we have to decode + */ +/obj/machinery/chem_recipe_debug/proc/extract_reaction_name(datum/chemical_reaction/reaction) + PRIVATE_PROC(TRUE) + SHOULD_BE_PURE(TRUE) + + var/reaction_name = "[reaction]" + reaction_name = copytext(reaction_name, findlasttext(reaction_name, "/") + 1) + reaction_name = replacetext(reaction_name, "_", " ") + return full_capitalize(reaction_name) + +///Retrives the target temperature to be imposed on the test reaction based on temp_mode +/obj/machinery/chem_recipe_debug/proc/decode_target_temperature() + PRIVATE_PROC(TRUE) + SHOULD_BE_PURE(TRUE) + + if(temp_mode == USE_REACTION_TEMPERATURE) + return null //simply means don't alter the reaction temperature + else if(temp_mode == USE_USER_TEMPERATURE) + return forced_temp + else + var/datum/chemical_reaction/test_reaction = reactions_to_test[current_reaction_index || 1] + switch(temp_mode) + if(USE_MINIMUM_TEMPERATURE) + return test_reaction.required_temp + (test_reaction.is_cold_recipe ? - 20 : 20) //20k is good enough offset to account for reaction rate rounding + if(USE_OPTIMAL_TEMPERATURE) + return test_reaction.optimal_temp + if(USE_OVERHEAT_TEMPERATURE) + return test_reaction.overheat_temp + + +/** + * Adjusts the temperature, ph & purity of the holder + * Arguments + * + * * seconds_per_tick - passed from on_reaction_step or process + */ +/obj/machinery/chem_recipe_debug/proc/adjust_environment(seconds_per_tick) + PRIVATE_PROC(TRUE) + + var/target_temperature = decode_target_temperature() + if(!isnull(target_temperature)) + target_reagents.adjust_thermal_energy((target_temperature - target_reagents.chem_temp) * 0.4 * seconds_per_tick * SPECIFIC_HEAT_DEFAULT * target_reagents.total_volume) + + if(use_forced_purity) + target_reagents.set_all_reagents_purity(forced_purity) + + if(use_forced_ph) + for(var/datum/reagent/reagent as anything in target_reagents.reagent_list) + reagent.ph = clamp(forced_ph, CHEMICAL_MIN_PH, CHEMICAL_MAX_PH) + + target_reagents.update_total() -/* -* The main loop that sets up, creates and displays results from a reaction -* warning: this code is a hot mess -*/ /obj/machinery/chem_recipe_debug/process(seconds_per_tick) - if(processing == FALSE) - setup_reactions() - if(should_force_ph) - reagents.ph = force_ph - if(should_force_temp) - reagents.chem_temp = force_temp - if(reagents.is_reacting == TRUE) - react_time += seconds_per_tick + if(!target_reagents.is_reacting) + adjust_environment(seconds_per_tick) + target_reagents.handle_reactions() + + //send updates to ui. faster than SStgui.update_uis + for(var/datum/tgui/ui in src.open_uis) + ui.send_update() + +/obj/machinery/chem_recipe_debug/proc/on_reaction_step(datum/reagents/holder, num_reactions, seconds_per_tick) + SIGNAL_HANDLER + + adjust_environment(seconds_per_tick) + + //send updates to ui. faster than SStgui.update_uis + for(var/datum/tgui/ui in src.open_uis) + ui.send_update() + +/** + * Decodes the ui reaction var into it's original name + * Arguments + * + * * variable - the name of the variable as seen in the UI + */ +/obj/machinery/chem_recipe_debug/proc/decode_var(variable) + PRIVATE_PROC(TRUE) + + . = null + + if(isnull(edit_reaction)) return - if(reaction_stated == TRUE) - reaction_stated = FALSE - relay_ended_reaction() - if(index > cached_reactions.len) - relay_all_reactions() - return - setup_reaction() - reaction_stated = TRUE -/obj/machinery/chem_recipe_debug/proc/relay_all_reactions() - say("Completed testing, missing reactions products (may have exploded) are:") - say("[problem_string]", sanitize=FALSE) - say("Problem with results are:") - say("[impure_string]", sanitize=FALSE) - say("Reactions with minor impurity: [minorImpurity], reactions with major impurity: [majorImpurity]") - processing = FALSE - problem_string = null - impure_string = null - minorImpurity = null - majorImpurity = null - end_processing() - -/obj/machinery/chem_recipe_debug/proc/relay_ended_reaction() - if(reagents.reagent_list) - var/cached_purity - say("Reaction completed for [cached_reactions[index]] final temperature = [reagents.chem_temp], ph = [reagents.ph], time taken = [react_time]s.") - var/datum/chemical_reaction/reaction = cached_reactions[index] - for(var/reagent_type in reaction.results) - var/datum/reagent/reagent = reagents.has_reagent(reagent_type) - if(!reagent) - say(span_warning("Unable to find product [reagent_type] in holder after reaction! reagents found are:")) - for(var/other_reagent in reagents.reagent_list) - say("[other_reagent]") - var/obj/item/reagent_containers/cup/beaker/bluespace/beaker = new /obj/item/reagent_containers/cup/beaker/bluespace(loc) - reagents.trans_to(beaker) - beaker.name = "[cached_reactions[index]] failed" - if(!failed) - problem_string += "[cached_reactions[index]] [span_warning("Unable to find product [reagent_type] in holder after reaction! Trying alternative setup. index:[index]")]\n" - failed++ - return - say("Reaction has a product [reagent_type] [reagent.volume]u purity of [reagent.purity]") - if(reagent.purity < 0.9) - impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [reagent.volume]u [span_boldwarning("purity of [reagent.purity]")] index:[index]\n" - majorImpurity++ - else if (reagent.purity < 1) - impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [reagent.volume]u [span_warning("purity of [reagent.purity]")] index:[index]\n" - minorImpurity++ - if(reagent.volume < reaction.results[reagent_type]) - impure_string += "Reaction [cached_reactions[index]] has a product [reagent_type] [span_warning("[reagent.volume]u")] purity of [reagent.purity] index:[index]\n" - cached_purity = reagent.purity - if(beaker_spawn && reagents.total_volume) - var/obj/item/reagent_containers/cup/beaker/bluespace/beaker = new /obj/item/reagent_containers/cup/beaker/bluespace(loc) - reagents.trans_to(beaker) - beaker.name = "[cached_reactions[index]] purity: [cached_purity]" - reagents.clear_reagents() - reagents.chem_temp = 300 - index++ - failed = 0 - else - say("No reagents left in beaker!") - index++ - -/obj/machinery/chem_recipe_debug/proc/setup_reaction() - react_time = 0 - if(!length(cached_reactions)) - return FALSE - var/datum/chemical_reaction/reaction = cached_reactions[index] - if(!reaction) - say("Unable to find reaction on index: [index]") - say("Using forced temperatures.") - if(reaction.reaction_flags & REACTION_INSTANT) - say("This reaction is instant") - for(var/reagent_type in reaction.required_reagents) - reagents.add_reagent(reagent_type, reaction.required_reagents[reagent_type]*vol_multiplier) - for(var/catalyst_type in reaction.required_catalysts) - reagents.add_reagent(catalyst_type, reaction.required_catalysts[catalyst_type]) - if(should_force_temp && !min_temp) - say("Using forced temperatures.") - reagents.chem_temp = force_temp ? force_temp : reaction.optimal_temp - if(should_force_ph) - say("Using forced pH.") - reagents.ph = force_ph ? force_ph : (reaction.optimal_ph_max + reaction.optimal_ph_min)/2 - if(failed == 0 && !should_force_temp) - reagents.chem_temp = reaction.optimal_temp - if(failed == 1 && !should_force_temp) - reagents.chem_temp = reaction.required_temp+25 - failed++ - if(min_temp) - say("Overriding temperature to required temp.") - reagents.chem_temp = reaction.is_cold_recipe ? reaction.required_temp - 1 : reaction.required_temp + 1 - say("Reacting [span_nicegreen("[cached_reactions[index]]")] starting pH: [reagents.ph] index [index] of [cached_reactions.len]") - -/obj/machinery/chem_recipe_debug/ui_data(mob/user) - var/data = list() - data["targetTemp"] = force_temp - data["targatpH"] = force_ph - data["isActive"] = reagents.is_reacting - data["forcepH"] = should_force_ph - data["forceTemp"] = should_force_temp - data["targetVol"] = vol_multiplier - data["processAll"] = process_all - data["currentTemp"] = reagents.chem_temp - data["currentpH"] = round(reagents.ph, 0.01) - data["processing"] = processing - data["index"] = index - data["endIndex"] = cached_reactions.len - data["beakerSpawn"] = beaker_spawn - data["minTemp"] = min_temp - data["editRecipe"] = null - - var/list/beaker_contents = list() - for(var/datum/reagent/reagent as anything in reagents.reagent_list) - beaker_contents.len++ - beaker_contents[length(beaker_contents)] = list("name" = reagent.name, "volume" = round(reagent.volume, 0.01), "purity" = round(reagent.purity)) - data["chamberContents"] = beaker_contents - - var/list/queued_reactions = list() - for(var/datum/chemical_reaction/reaction as anything in reaction_names) - var/datum/reagent/reagent = find_reagent_object_from_type(reaction.results[1]) - queued_reactions.len++ - queued_reactions[length(queued_reactions)] = list("name" = reagent.name) - data["queuedReactions"] = queued_reactions - - var/list/active_reactions = list() - var/flashing = DISABLE_FLASHING //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false - for(var/datum/equilibrium/equilibrium as anything in reagents.reaction_list) - if(!length(reagents.reaction_list))//I'm not sure why when it explodes it causes the gui to fail (it's missing danger (?) ) - stack_trace("Chem debug managed to find an equilibrium in a location where there should be none (skipping this entry and continuing). This is usually because of an ill timed explosion.") - continue - if(!equilibrium.reaction.results)//Incase of no result reactions - continue - var/datum/reagent/reagent = reagents.has_reagent(equilibrium.reaction.results[1]) //Reactions are named after their primary products - if(!reagent) - continue - var/overheat = FALSE - var/danger = FALSE - var/purity_alert = 2 //same as flashing - if(reagent.purity < equilibrium.reaction.purity_min) - purity_alert = ENABLE_FLASHING//Because 0 is seen as null - danger = TRUE - if(flashing != ENABLE_FLASHING)//So that the pH meter flashes for ANY reactions out of optimal - if(equilibrium.reaction.optimal_ph_min > reagents.ph || equilibrium.reaction.optimal_ph_max < reagents.ph) - flashing = ENABLE_FLASHING - if(equilibrium.reaction.is_cold_recipe) - if(equilibrium.reaction.overheat_temp > reagents.chem_temp && equilibrium.reaction.overheat_temp != NO_OVERHEAT) - danger = TRUE - overheat = TRUE - else - if(equilibrium.reaction.overheat_temp < reagents.chem_temp) - danger = TRUE - overheat = TRUE - if(equilibrium.reaction.reaction_flags & REACTION_COMPETITIVE) //We have a compeitive reaction - concatenate the results for the different reactions - for(var/entry in active_reactions) - if(entry["name"] == reagent.name) //If we have multiple reaction methods for the same result - combine them - entry["reactedVol"] = equilibrium.reacted_vol - entry["targetVol"] = round(equilibrium.target_vol, 1)//Use the first result reagent to name the reaction detected - entry["quality"] = (entry["quality"] + equilibrium.reaction_quality) /2 - continue - active_reactions.len++ - active_reactions[length(active_reactions)] = list("name" = reagent.name, "danger" = danger, "purityAlert" = purity_alert, "quality" = equilibrium.reaction_quality, "overheat" = overheat, "inverse" = reagent.inverse_chem_val, "minPure" = equilibrium.reaction.purity_min, "reactedVol" = equilibrium.reacted_vol, "targetVol" = round(equilibrium.target_vol, 1))//Use the first result reagent to name the reaction detected - data["activeReactions"] = active_reactions - data["isFlashing"] = flashing - - if(edit_recipe) - data["editRecipeName"] = edit_recipe.type - data["editRecipeCold"] = edit_recipe.is_cold_recipe - data["editRecipe"] = list( - list("name" = "required_temp" , "var" = edit_recipe.required_temp), - list("name" = "optimal_temp" , "var" = edit_recipe.optimal_temp), - list("name" = "overheat_temp" , "var" = edit_recipe.overheat_temp), - list("name" = "optimal_ph_min" , "var" = edit_recipe.optimal_ph_min), - list("name" = "optimal_ph_max" , "var" = edit_recipe.optimal_ph_max), - list("name" = "determin_ph_range" , "var" = edit_recipe.determin_ph_range), - list("name" = "temp_exponent_factor" , "var" = edit_recipe.temp_exponent_factor), - list("name" = "ph_exponent_factor" , "var" = edit_recipe.ph_exponent_factor), - list("name" = "thermic_constant" , "var" = edit_recipe.thermic_constant), - list("name" = "H_ion_release" , "var" = edit_recipe.H_ion_release), - list("name" = "rate_up_lim" , "var" = edit_recipe.rate_up_lim), - list("name" = "purity_min" , "var" = edit_recipe.purity_min), + var/static/list/ui_to_var + if(isnull(ui_to_var)) + ui_to_var = list( + "Required Temp" = "required_temp", + "Optimal Temp" = "optimal_temp", + "Overheat Temp" = "overheat_temp", + "Optimal Min Ph" = "optimal_ph_min", + "Optimal Max Ph" = "optimal_ph_max", + "Ph Range" = "determin_ph_range", + "Temp Exp Factor" = "temp_exponent_factor", + "Ph Exp Factor" = "ph_exponent_factor", + "Thermic Constant" = "thermic_constant", + "H Ion Release" = "H_ion_release", + "Rate Up Limit" = "rate_up_lim", + "Purity Min" = "purity_min", ) - return data - -/obj/machinery/chem_recipe_debug/ui_act(action, params) - . = ..() - if(.) - return - switch(action) - if("power") - return - if("temperature") - var/target = params["target"] - if(text2num(target) != null) - target = text2num(target) - . = TRUE - if(.) - force_temp = clamp(target, 0, 1000) - if("pH") - var/target = params["target"] - if(text2num(target) != null) - target = text2num(target) - . = TRUE - if(.) - force_ph = target - if("forceTemp") - should_force_temp = ! should_force_temp - . = TRUE - if("forcepH") - should_force_ph = ! should_force_ph - . = TRUE - if("react") - react = TRUE - return TRUE - if("all") - process_all = !process_all - return TRUE - if("beakerSpawn") - beaker_spawn = !beaker_spawn - return TRUE - if("setTargetList") - var/text = tgui_input_text(usr, "Enter a list of Recipe product names separated by commas", "Recipe List", multiline = TRUE) - reaction_names = list() - if(!text) - say("Could not find reaction") - var/list/names = splittext("[text]", ",") - for(var/name in names) - var/datum/reagent/reagent = find_reagent_object_from_type(get_chem_id(name)) - if(!reagent) - say("Could not find [name]") - continue - var/datum/chemical_reaction/reaction = GLOB.chemical_reactions_list_product_index[reagent.type] - if(!reaction) - say("Could not find [name] reaction!") - continue - reaction_names += reaction - if("vol") - var/target = params["target"] - if(text2num(target) != null) - target = text2num(target) - . = TRUE - if(.) - vol_multiplier = clamp(target, 1, 200) - if("start") - if(processing) - say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]") - return - say("Starting processing") - index = 1 - setup_reactions() - begin_processing() - return TRUE - if("stop") - relay_all_reactions() - if("minTemp") - min_temp = !min_temp - if("setEdit") - var/name = (input("Enter the name of any reagent", "Input") as text|null) - reaction_names = list() - if(!text) - say("Could not find reaction") - var/datum/reagent/reagent = find_reagent_object_from_type(get_chem_id(name)) - if(!reagent) - say("Could not find [name]") - return - var/datum/chemical_reaction/reaction = GLOB.chemical_reactions_list_product_index[reagent.type] - if(!reaction) - say("Could not find [name] reaction!") - return - edit_recipe = reaction[1] - if("updateVar") - var/target = params["target"] - edit_recipe.vars[params["type"]] = target - if("export") - var/export = {"[edit_recipe.type] -[edit_recipe.is_cold_recipe ? "is_cold_recipe = TRUE" : ""] -required_temp = [edit_recipe.required_temp] -optimal_temp = [edit_recipe.optimal_temp] -overheat_temp = [edit_recipe.overheat_temp] -optimal_ph_min = [edit_recipe.optimal_ph_min] -optimal_ph_max = [edit_recipe.optimal_ph_max] -determin_ph_range = [edit_recipe.determin_ph_range] -temp_exponent_factor = [edit_recipe.temp_exponent_factor] -ph_exponent_factor = [edit_recipe.ph_exponent_factor] -thermic_constant = [edit_recipe.thermic_constant] -H_ion_release = [edit_recipe.H_ion_release] -rate_up_lim = [edit_recipe.rate_up_lim] -purity_min = [edit_recipe.purity_min]"} - say(export) - text2file(export, "[GLOB.log_directory]/chem_parse.txt") - + var/value = ui_to_var[variable] + if(!isnull(value)) + . = value /obj/machinery/chem_recipe_debug/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) @@ -397,10 +243,443 @@ purity_min = [edit_recipe.purity_min]"} ui = new(user, src, "ChemRecipeDebug", name) ui.open() -///Moves a type of buffer from the heater to the beaker, +/obj/machinery/chem_recipe_debug/ui_data(mob/user) + . = list() -/obj/machinery/chem_recipe_debug/ui_status(mob/user) - return UI_INTERACTIVE + .["forced_temp"] = forced_temp + .["temp_mode"] = temp_mode + .["forced_ph"] = forced_ph + .["use_forced_ph"] = use_forced_ph + .["forced_purity"] = forced_purity + .["use_forced_purity"] = use_forced_purity + .["volume_multiplier"] = volume_multiplier -/obj/machinery/chem_recipe_debug/ui_state(mob/user) - return GLOB.physical_state + var/datum/chemical_reaction/current_reaction = null + if(reactions_to_test.len) + current_reaction = reactions_to_test[current_reaction_index || 1] + if(isnull(current_reaction)) + .["current_reaction_name"] = "N/A" + else + .["current_reaction_name"] = extract_reaction_name(current_reaction) + .["current_reaction_mode"] = current_reaction_mode + + var/list/active_reactions = list() + var/flashing = DISABLE_FLASHING //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false + for(var/datum/equilibrium/equilibrium as anything in target_reagents.reaction_list) + if(!equilibrium.reaction.results)//Incase of no result reactions + continue + var/datum/reagent/reagent = target_reagents.has_reagent(equilibrium.reaction.results[1]) //Reactions are named after their primary products + if(!reagent) + continue + + //check for danger levels primirarly overheating + var/overheat = FALSE + var/danger = FALSE + var/purity_alert = 2 //same as flashing + if(reagent.purity < equilibrium.reaction.purity_min) + purity_alert = ENABLE_FLASHING//Because 0 is seen as null + danger = TRUE + if(flashing != ENABLE_FLASHING)//So that the pH meter flashes for ANY reactions out of optimal + if(equilibrium.reaction.optimal_ph_min > target_reagents.ph || equilibrium.reaction.optimal_ph_max < target_reagents.ph) + flashing = ENABLE_FLASHING + if(equilibrium.reaction.is_cold_recipe) + if(equilibrium.reaction.overheat_temp > target_reagents.chem_temp && equilibrium.reaction.overheat_temp != NO_OVERHEAT) + danger = TRUE + overheat = TRUE + else + if(equilibrium.reaction.overheat_temp < target_reagents.chem_temp) + danger = TRUE + overheat = TRUE + + //create ui data + active_reactions += list(list( + "name" = reagent.name, + "danger" = danger, + "overheat" = overheat, + "purityAlert" = purity_alert, + "quality" = equilibrium.reaction_quality, + "inverse" = reagent.inverse_chem_val, + "minPure" = equilibrium.reaction.purity_min, + "reactedVol" = equilibrium.reacted_vol, + "targetVol" = round(equilibrium.target_vol, 1) + ) + ) + + //additional data for competitive reactions + if(equilibrium.reaction.reaction_flags & REACTION_COMPETITIVE) //We have a compeitive reaction - concatenate the results for the different reactions + for(var/entry in active_reactions) + if(entry["name"] == reagent.name) //If we have multiple reaction methods for the same result - combine them + entry["reactedVol"] = equilibrium.reacted_vol + entry["targetVol"] = round(equilibrium.target_vol, 1)//Use the first result reagent to name the reaction detected + entry["quality"] = (entry["quality"] + equilibrium.reaction_quality) /2 + continue + .["activeReactions"] = active_reactions + + .["isFlashing"] = flashing + .["isReacting"] = target_reagents.is_reacting + + var/list/reaction_data = null + if(!isnull(edit_reaction)) + var/reaction_name + if(length(edit_reaction.results)) //soups can have no results + var/datum/reagent/reagent = edit_reaction.results[1] + reaction_name = initial(reagent.name) + else + reaction_name = "[edit_reaction]" + reaction_data = list( + "name" = reaction_name, + "editVar" = edit_var, + "editValue" = edit_reaction.vars[decode_var(edit_var)] + ) + .["editReaction"] = reaction_data + + var/list/beaker_data = null + if(target_reagents.reagent_list.len) + beaker_data = list() + beaker_data["maxVolume"] = target_reagents.maximum_volume + beaker_data["pH"] = round(target_reagents.ph, 0.01) + beaker_data["purity"] = round(target_reagents.get_average_purity(), 0.01) + beaker_data["currentVolume"] = round(target_reagents.total_volume, CHEMICAL_VOLUME_ROUNDING) + beaker_data["currentTemp"] = round(target_reagents.chem_temp, 1) + beaker_data["purity"] = round(target_reagents.get_average_purity(), 0.001) + var/list/beakerContents = list() + if(length(target_reagents.reagent_list)) + for(var/datum/reagent/reagent in target_reagents.reagent_list) + beakerContents += list(list("name" = reagent.name, "volume" = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING))) + + if(!QDELETED(required_container)) + //as of now we only decode soup pots. If more exotic containers are made make sure to add them here + if(istype(required_container, /obj/item/reagent_containers/cup/soup_pot)) + var/obj/item/reagent_containers/cup/soup_pot/pot = required_container + for(var/obj/item as anything in pot.added_ingredients) + //increment count if item already exists + var/entry_found = FALSE + for(var/list/entry as anything in beakerContents) + if(entry["name"] == item.name) + entry["volume"] += 1 + entry_found = TRUE + break + //new entry if non existent + if(!entry_found) + beakerContents += list(list("name" = item.name, "volume" = 1)) + + beaker_data["contents"] = beakerContents + .["beaker"] = beaker_data + +/obj/machinery/chem_recipe_debug/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("forced_temp") + var/target = params["target"] + if(isnull(target)) + return + + target = text2num(target) + if(isnull(target)) + return + + forced_temp = target + return TRUE + + if("temp_mode") + var/target = params["target"] + if(isnull(target)) + return + + switch(target) + if("Reaction Temp") + temp_mode = USE_REACTION_TEMPERATURE + return TRUE + if("Forced Temp") + temp_mode = USE_USER_TEMPERATURE + return TRUE + if("Minimum Temp") + temp_mode = USE_MINIMUM_TEMPERATURE + return TRUE + if("Optimal Temp") + temp_mode = USE_OPTIMAL_TEMPERATURE + return TRUE + if("Overheat Temp") + temp_mode = USE_OVERHEAT_TEMPERATURE + return TRUE + + if("forced_ph") + var/target = params["target"] + if(isnull(target)) + return + + target = text2num(target) + if(isnull(target)) + return + + forced_ph = target + return TRUE + + if("toggle_forced_ph") + use_forced_ph = !use_forced_ph + return TRUE + + if("forced_purity") + var/target = params["target"] + if(isnull(target)) + return + + target = text2num(target) + if(isnull(target)) + return + + forced_purity = target + return TRUE + + if("toggle_forced_purity") + use_forced_purity = !use_forced_purity + return TRUE + + if("volume_multiplier") + var/target = params["target"] + if(isnull(target)) + return + + target = text2num(target) + if(isnull(target)) + return + + volume_multiplier = target + return TRUE + + if("pick_reaction") + var/mode = tgui_alert(usr, "Play all or an specific reaction?","Select Reaction", list("All", "Specific")) + if(mode == "All") + reactions_to_test.Cut() + for(var/reaction as anything in all_reaction_list) + reactions_to_test += all_reaction_list[reaction] + current_reaction_index = 0 + return TRUE + + var/selected_reaction = tgui_input_list(ui.user, "Select Reaction", "Reaction", all_reaction_list) + if(!selected_reaction) + return + + var/datum/chemical_reaction/reaction = all_reaction_list[selected_reaction] + if(!reaction) + return + + reactions_to_test.Cut() + reactions_to_test += reaction + current_reaction_index = 0 + return TRUE + + if("reaction_mode") + var/target = params["target"] + if(isnull(target)) + return + + switch(target) + if("Next Reaction") + current_reaction_mode = PLAY_NEXT_REACTION + return TRUE + if("Previous Reaction") + current_reaction_mode = PLAY_PREVIOUS_REACTION + return TRUE + if("Pick Reaction") + current_reaction_mode = PLAY_USER_REACTION + return TRUE + + if("start_reaction") + var/datum/chemical_reaction/test_reaction + + //pick the reaction based on the reaction mode + var/len = reactions_to_test.len + if(len > 1) + switch(current_reaction_mode) + if(PLAY_NEXT_REACTION) + current_reaction_index = (current_reaction_index + 1) % len + if(PLAY_PREVIOUS_REACTION) + current_reaction_index = max(current_reaction_index - 1, 1) + if(PLAY_USER_REACTION) + var/list/reaction_names = list() + for(var/datum/chemical_reaction/reaction as anything in reactions_to_test) + reaction_names += extract_reaction_name(reaction) + if(!reaction_names.len) + return + + var/selected_reaction = tgui_input_list(ui.user, "Select Reaction", "Reaction", reaction_names) + if(!selected_reaction) + return + for(var/i = 1; i <= reaction_names.len; i++) + if(selected_reaction == reaction_names[i]) + current_reaction_index = i + break + test_reaction = reactions_to_test[current_reaction_index] + else if(len == 1) + current_reaction_index = 1 + test_reaction = reactions_to_test[1] + + //clear the previous reaction stuff + target_reagents.force_stop_reacting() + target_reagents.clear_reagents() + + //If the reaction requires a specific container initialize & do other stuff accordingly + target_reagents = reagents + if(!QDELETED(required_container)) + UnregisterSignal(required_container.reagents, COMSIG_REAGENTS_REACTION_STEP) + QDEL_NULL(required_container) + if(!isnull(test_reaction.required_container)) + required_container = new test_reaction.required_container(src) + required_container.create_reagents(MAXIMUM_HOLDER_VOLUME) + target_reagents = required_container.reagents + RegisterSignal(target_reagents, COMSIG_REAGENTS_REACTION_STEP, TYPE_PROC_REF(/obj/machinery/chem_recipe_debug, on_reaction_step)) + + //append everything required + var/list/reagent_list = list() + if(length(test_reaction.required_catalysts)) + reagent_list += test_reaction.required_catalysts + if(length(test_reaction.required_reagents)) + reagent_list += test_reaction.required_reagents + //now add the required reagents + var/target_temperature + switch(temp_mode) + if(USE_REACTION_TEMPERATURE) + target_temperature = DEFAULT_REAGENT_TEMPERATURE + else + target_temperature = decode_target_temperature() + for(var/datum/reagent/_reagent as anything in reagent_list) + var/vol_mul = volume_multiplier + if(length(test_reaction.required_catalysts) && test_reaction.required_catalysts[_reagent.type]) + vol_mul = 1 //catalysts don't need to be present in large amounts + + //add the required reagents with the precise conditions + target_reagents.add_reagent( + _reagent, + reagent_list[_reagent] * vol_mul, + reagtemp = target_temperature, + added_purity = use_forced_purity ? forced_purity : null, + added_ph = use_forced_ph ? forced_ph : null, + no_react = TRUE + ) + + //add solid ingredients for soups + if(istype(test_reaction, /datum/chemical_reaction/food/soup)) + var/datum/chemical_reaction/food/soup/soup_reaction = test_reaction + var/obj/item/reagent_containers/cup/soup_pot/pot = required_container + for(var/obj/item as anything in soup_reaction.required_ingredients) + for(var/_ in 1 to soup_reaction.required_ingredients[item]) + LAZYADD(pot.added_ingredients, new item(pot)) + + target_reagents.handle_reactions() + return TRUE + + if("edit_reaction") + var/selected_reaction = tgui_input_list(ui.user, "Select Reaction", "Reaction", all_reaction_list) + if(!selected_reaction) + return + + var/datum/chemical_reaction/reaction = all_reaction_list[selected_reaction] + if(!reaction) + return + + edit_reaction = reaction + edit_var = initial(edit_var) + return TRUE + + if("edit_var") + var/target = params["target"] + if(isnull(target)) + return + if(isnull(decode_var(target))) + return + edit_var = target + return TRUE + + if("edit_value") + var/target = params["target"] + if(isnull(target)) + return + + target = text2num(target) + if(isnull(target)) + return + + edit_reaction.vars[decode_var(edit_var)] = target + return TRUE + + if("reset_value") + switch(edit_var) + if("Required Temp") + edit_reaction.required_temp = initial(edit_reaction.required_temp) + return TRUE + if("Optimal Temp") + edit_reaction.optimal_temp = initial(edit_reaction.optimal_temp) + return TRUE + if("Overheat Temp") + edit_reaction.overheat_temp = initial(edit_reaction.overheat_temp) + return TRUE + if("Optimal Min Ph") + edit_reaction.optimal_ph_min = initial(edit_reaction.optimal_ph_min) + return TRUE + if("Optimal Max Ph") + edit_reaction.optimal_ph_max = initial(edit_reaction.optimal_ph_max) + return TRUE + if("Ph Range") + edit_reaction.determin_ph_range = initial(edit_reaction.determin_ph_range) + return TRUE + if("Temp Exp Factor") + edit_reaction.temp_exponent_factor = initial(edit_reaction.temp_exponent_factor) + return TRUE + if("Ph Exp Factor") + edit_reaction.ph_exponent_factor = initial(edit_reaction.ph_exponent_factor) + return TRUE + if("Thermic Constant") + edit_reaction.thermic_constant = initial(edit_reaction.thermic_constant) + return TRUE + if("H Ion Release") + edit_reaction.H_ion_release = initial(edit_reaction.H_ion_release) + return TRUE + if("Rate Up Limit") + edit_reaction.rate_up_lim = initial(edit_reaction.rate_up_lim) + return TRUE + if("Purity Min") + edit_reaction.purity_min = initial(edit_reaction.purity_min) + return TRUE + + if("export") + var/export = "[edit_reaction]\n" + export += "\tis_cold_recipe = [edit_reaction.is_cold_recipe]\n" + export += "\trequired_temp = [edit_reaction.required_temp]\n" + export += "\toptimal_temp = [edit_reaction.optimal_temp]\n" + export += "\toverheat_temp = [edit_reaction.overheat_temp]\n" + export += "\toptimal_ph_min = [edit_reaction.optimal_ph_min]\n" + export += "\toptimal_ph_max = [edit_reaction.optimal_ph_max]\n" + export += "\tdetermin_ph_range = [edit_reaction.determin_ph_range]\n" + export += "\ttemp_exponent_factor = [edit_reaction.temp_exponent_factor]\n" + export += "\tph_exponent_factor = [edit_reaction.ph_exponent_factor]\n" + export += "\tthermic_constant = [edit_reaction.thermic_constant]\n" + export += "\tH_ion_release = [edit_reaction.H_ion_release]\n" + export += "\trate_up_lim = [edit_reaction.rate_up_lim]\n" + export += "\tpurity_min = [edit_reaction.purity_min]\n" + + var/dest = "[GLOB.log_directory]/chem_parse.txt" + text2file(export, dest) + tgui_alert(ui.user, "Saved to [dest]") + + if("eject") + if(!target_reagents.total_volume) + return + if(QDELETED(beaker)) + beaker = new /obj/item/reagent_containers/cup/beaker/bluespace(src) + target_reagents.trans_to(beaker, target_reagents.total_volume) + try_put_in_hand(beaker, ui.user) + return TRUE + +#undef USE_REACTION_TEMPERATURE +#undef USE_MINIMUM_TEMPERATURE +#undef USE_USER_TEMPERATURE +#undef USE_OPTIMAL_TEMPERATURE +#undef USE_OVERHEAT_TEMPERATURE +#undef PLAY_NEXT_REACTION +#undef PLAY_PREVIOUS_REACTION +#undef PLAY_USER_REACTION +#undef MAXIMUM_HOLDER_VOLUME diff --git a/icons/obj/medical/cryogenics.dmi b/icons/obj/medical/cryogenics.dmi index bd82d4bbacee034d818ca508fb13c0e41c04ade7..4dd33af8ce7b42da77dcc28e583a6bba7acae114 100644 GIT binary patch literal 21449 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

`hHnzpfZn^JUW^RPc4pShBYg{N950JT=?p)7o@# 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 literal 42361 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 { diff --git a/tgui/packages/tgui/interfaces/ChemHeater.tsx b/tgui/packages/tgui/interfaces/ChemHeater.tsx index 6ac5d757ca1..d9d61c92592 100644 --- a/tgui/packages/tgui/interfaces/ChemHeater.tsx +++ b/tgui/packages/tgui/interfaces/ChemHeater.tsx @@ -17,7 +17,7 @@ import { COLORS } from '../constants'; import { Window } from '../layouts'; import { Beaker, BeakerSectionDisplay } from './common/BeakerDisplay'; -type ActiveReaction = { +export type ActiveReaction = { name: string; danger: BooleanLike; overheat: BooleanLike; @@ -42,6 +42,144 @@ type Data = { dispenseVolume: number; }; +type ReactionDisplayProps = { + beaker: Beaker; + isFlashing: number; + activeReactions: ActiveReaction[]; + highQualityDisplay: BooleanLike; + highDangerDisplay: BooleanLike; +}; + +export const ReactionDisplay = (props: ReactionDisplayProps) => { + const { + beaker, + isFlashing, + activeReactions, + highQualityDisplay, + highDangerDisplay, + } = props; + + return ( +

+ + 'pH: ' + round(value, 3)} + /> + + + ''} + ranges={{ + red: [-0.22, 1.5], + orange: [1.5, 3], + yellow: [3, 4.5], + olive: [4.5, 5], + good: [5, 6], + green: [6, 8.5], + teal: [8.5, 9.5], + blue: [9.5, 11], + purple: [11, 12.5], + violet: [12.5, 14], + }} + /> + + + } + > + {(activeReactions.length === 0 && ( + No active reactions. + )) || ( + + + + Reaction + + + {!highQualityDisplay ? 'Status' : 'Reaction quality'} + + + Target + + + {activeReactions.map((reaction) => ( + + + {reaction.name} + + + {!highQualityDisplay ? ( + + ) : ( + ''} + ml={5} + ranges={{ + red: [0, reaction.minPure], + orange: [reaction.minPure, reaction.inverse], + yellow: [reaction.inverse, 0.8], + green: [0.8, 1], + }} + /> + )} + + + {highDangerDisplay ? ( + <> + {!!reaction.overheat && ( + + )} + + {reaction.targetVol}u + + + ) : ( + + {reaction.targetVol}u + + )} + + + ))} + +
+ )} +
+ ); +}; + export const ChemHeater = (props) => { const { act, data } = useBackend(); const { @@ -208,125 +346,13 @@ export const ChemHeater = (props) => { {!!isBeakerLoaded && ( -
- - 'pH: ' + round(value, 3)} - /> - - - ''} - ranges={{ - red: [-0.22, 1.5], - orange: [1.5, 3], - yellow: [3, 4.5], - olive: [4.5, 5], - good: [5, 6], - green: [6, 8.5], - teal: [8.5, 9.5], - blue: [9.5, 11], - purple: [11, 12.5], - violet: [12.5, 14], - }} - /> - - - } - > - {(activeReactions.length === 0 && ( - No active reactions. - )) || ( - - - - Reaction - - - {upgradeLevel < 4 ? 'Status' : 'Reaction quality'} - - - Target - - - {activeReactions.map((reaction) => ( - - - {reaction.name} - - - {(upgradeLevel < 4 && ( - - )) || ( - ''} - ml={5} - ranges={{ - red: [0, reaction.minPure], - orange: [reaction.minPure, reaction.inverse], - yellow: [reaction.inverse, 0.8], - green: [0.8, 1], - }} - /> - )} - - - {(upgradeLevel > 2 && ( - <> - {!!reaction.overheat && ( - - )} - - {reaction.targetVol}u - - - )) || ( - - {reaction.targetVol}u - - )} - - - ))} - -
- )} -
+ = 4} + highDangerDisplay={upgradeLevel >= 2} + /> )} diff --git a/tgui/packages/tgui/interfaces/ChemRecipeDebug.jsx b/tgui/packages/tgui/interfaces/ChemRecipeDebug.jsx deleted file mode 100644 index ed47c6f95b8..00000000000 --- a/tgui/packages/tgui/interfaces/ChemRecipeDebug.jsx +++ /dev/null @@ -1,326 +0,0 @@ -import { round } from 'common/math'; - -import { useBackend } from '../backend'; -import { - AnimatedNumber, - Box, - Button, - Flex, - LabeledList, - NumberInput, - ProgressBar, - RoundGauge, - Section, - Table, -} from '../components'; -import { Window } from '../layouts'; -import { BeakerContents } from './common/BeakerContents'; - -export const ChemRecipeDebug = (props) => { - const { act, data } = useBackend(); - const { - targetTemp, - isActive, - isFlashing, - currentTemp, - currentpH, - forcepH, - forceTemp, - targetVol, - targatpH, - processing, - processAll, - index, - endIndex, - beakerSpawn, - minTemp, - editRecipeName, - editRecipeCold, - editRecipe = [], - chamberContents = [], - activeReactions = [], - queuedReactions = [], - } = data; - return ( - - -
-
-
- - -
-
- - 'pH: ' + round(value, 3)} - /> - - - ''} - ranges={{ - red: [-0.22, 1.5], - orange: [1.5, 3], - yellow: [3, 4.5], - olive: [4.5, 5], - good: [5, 6], - green: [6, 8.5], - teal: [8.5, 9.5], - blue: [9.5, 11], - purple: [11, 12.5], - violet: [12.5, 14], - }} - /> - - - } - > - {(activeReactions.length === 0 && ( - No active reactions. - )) || ( - - - - Reaction - - - {'Reaction quality'} - - - Target - - - {activeReactions && - activeReactions.map((reaction) => ( - - - {reaction.name} - - - ''} - ml={5} - ranges={{ - red: [0, reaction.minPure], - orange: [reaction.minPure, reaction.inverse], - yellow: [reaction.inverse, 0.8], - green: [0.8, 1], - }} - /> - - - - {reaction.targetVol}u - - - - ))} - -
- )} -
-
{isActive ? 'Reacting' : 'Waiting'}} - > - {(chamberContents.length && ( - - )) || Nothing} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/ChemRecipeDebug.tsx b/tgui/packages/tgui/interfaces/ChemRecipeDebug.tsx new file mode 100644 index 00000000000..c108966bbb7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChemRecipeDebug.tsx @@ -0,0 +1,437 @@ +import { BooleanLike } from 'common/react'; +import { useState } from 'react'; + +import { useBackend } from '../backend'; +import { + Box, + Button, + Dropdown, + LabeledList, + NumberInput, + Section, + Stack, + Tabs, +} from '../components'; +import { Window } from '../layouts'; +import { ActiveReaction, ReactionDisplay } from './ChemHeater'; +import { Beaker, BeakerSectionDisplay } from './common/BeakerDisplay'; + +const TEMP_MODES = [ + 'Reaction Temp', + 'Forced Temp', + 'Minimum Temp', + 'Optimal Temp', + 'Overheat Temp', +]; +const REACTION_MODES = ['Next Reaction', 'Previous Reaction', 'Pick Reaction']; +const REACTION_VARS = [ + 'Required Temp', + 'Optimal Temp', + 'Overheat Temp', + 'Optimal Min Ph', + 'Optimal Max Ph', + 'Ph Range', + 'Temp Exp Factor', + 'Ph Exp Factor', + 'Thermic Constant', + 'H Ion Release', + 'Rate Up Limit', + 'Purity Min', +]; + +type BeakerDebug = Beaker & { + currentTemp: number; + purity: number; +}; + +type Reaction = { + name: string; + editVar: string; + editValue: number; +}; + +type Data = { + forced_temp: number; + temp_mode: number; + forced_ph: number; + use_forced_ph: BooleanLike; + forced_purity: number; + use_forced_purity: number; + volume_multiplier: number; + isReacting: BooleanLike; + current_reaction_name: string; + current_reaction_mode: number; + beaker: BeakerDebug; + isFlashing: number; + activeReactions: ActiveReaction[]; + editReaction: Reaction; +}; + +export const ChemRecipeDebug = (props) => { + const { act, data } = useBackend(); + const [controlState, setControlState] = useState('Environment'); + const { + forced_temp, + temp_mode, + forced_ph, + use_forced_ph, + forced_purity, + use_forced_purity, + volume_multiplier, + isReacting, + current_reaction_name, + current_reaction_mode, + beaker, + isFlashing, + activeReactions, + editReaction, + } = data; + return ( + + +
+ + setControlState('Environment')} + > + Environment + + setControlState('Reactions')} + > + Reactions + + setControlState('Editing')} + > + Edit Reactions + + + {controlState === 'Environment' && ( +
+ + + + + + act('forced_temp', { + target: value, + }) + } + /> + + + + + + + Temp Mode: + + } + > + + act('temp_mode', { + target: value, + }) + } + /> + + + + + + + + PH:}> + + act('forced_ph', { + target: value, + }) + } + /> + + + + + + + act('toggle_forced_ph')} + > + {use_forced_ph ? 'Disable' : 'Enable'} + + + + + + + + + Purity:}> + + act('forced_purity', { + target: value, + }) + } + /> + + + + + + + act('toggle_forced_purity')} + > + {use_forced_purity ? 'Disable' : 'Enable'} + + + + + + + + + + + act('volume_multiplier', { + target: value, + }) + } + /> + + + + +
+ )} + {controlState === 'Reactions' && ( +
+ + + + + + + + + + + + {current_reaction_name} + + + + + + + Direction: + + } + > + + act('reaction_mode', { + target: value, + }) + } + /> + + + + + + Process:}> + + + + + +
+ )} + {controlState === 'Editing' && ( +
+ + + + + + + + + + + + Param: + + } + > + + act('edit_var', { target: value }) + } + disabled={editReaction === null} + /> + + + + + + Value:}> + + act('edit_value', { + target: value, + }) + } + /> + + + + + +
+ )} +
+ {beaker && ( +
+ + + {beaker.currentTemp} + + + {beaker.purity} + + +
+ )} + {beaker && ( + + )} + +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Cryo.jsx b/tgui/packages/tgui/interfaces/Cryo.jsx deleted file mode 100644 index de547e93fba..00000000000 --- a/tgui/packages/tgui/interfaces/Cryo.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useBackend } from '../backend'; -import { - AnimatedNumber, - Button, - LabeledList, - ProgressBar, - Section, -} from '../components'; -import { Window } from '../layouts'; -import { BeakerContents } from './common/BeakerContents'; - -const damageTypes = [ - { - label: 'Brute', - type: 'bruteLoss', - }, - { - label: 'Respiratory', - type: 'oxyLoss', - }, - { - label: 'Toxin', - type: 'toxLoss', - }, - { - label: 'Burn', - type: 'fireLoss', - }, -]; - -export const Cryo = () => { - return ( - - - - - - ); -}; - -const CryoContent = (props) => { - const { act, data } = useBackend(); - return ( - <> -
- - - {data.occupant.name || 'No Occupant'} - - {!!data.hasOccupant && ( - <> - - {data.occupant.stat} - - - - {' K'} - - - 0 ? 'good' : 'average'} - > - - - - {damageTypes.map((damageType) => ( - - - - - - ))} - - )} - -
-
- - - - - - K - - -
-
act('ejectbeaker')} - content="Eject" - /> - } - > - -
- - ); -}; diff --git a/tgui/packages/tgui/interfaces/Cryo.tsx b/tgui/packages/tgui/interfaces/Cryo.tsx new file mode 100644 index 00000000000..2999f4c04d8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Cryo.tsx @@ -0,0 +1,150 @@ +import { round } from 'common/math'; +import { BooleanLike } from 'common/react'; + +import { useBackend } from '../backend'; +import { + AnimatedNumber, + Button, + LabeledList, + ProgressBar, + Section, +} from '../components'; +import { Window } from '../layouts'; +import { Beaker, BeakerSectionDisplay } from './common/BeakerDisplay'; + +const damageTypes = [ + { + label: 'Brute', + type: 'bruteLoss', + }, + { + label: 'Respiratory', + type: 'oxyLoss', + }, + { + label: 'Toxin', + type: 'toxLoss', + }, + { + label: 'Burn', + type: 'fireLoss', + }, +] as const; + +const stat_to_color = { + Dead: 'bad', + Conscious: 'bad', + Unconscious: 'good', +} as const; + +type Occupant = { + name: string; + stat: string; + bodyTemperature: number; + health: number; + maxHealth: number; + bruteLoss: number; + oxyLoss: number; + toxLoss: number; + fireLoss: number; +}; + +type Data = { + isOperating: BooleanLike; + isOpen: BooleanLike; + autoEject: BooleanLike; + occupant: Occupant; + T0C: number; + cellTemperature: number; + beaker: Beaker; +}; + +export const Cryo = () => { + const { act, data } = useBackend(); + const { occupant, isOperating, isOpen } = data; + + return ( + + +
+ + + {occupant?.name || 'No Occupant'} + + {!!occupant && ( + <> + + {occupant.stat} + + + + {' K'} + + + 0 ? 'good' : 'average'} + > + + + + {damageTypes.map((damageType) => ( + + + + + + ))} + + )} + +
+
+ + + + + + K + + + + + + +
+ +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PortableChemMixer.tsx b/tgui/packages/tgui/interfaces/PortableChemMixer.tsx index 15cbb56ba92..fc0b4cbb95e 100644 --- a/tgui/packages/tgui/interfaces/PortableChemMixer.tsx +++ b/tgui/packages/tgui/interfaces/PortableChemMixer.tsx @@ -12,10 +12,14 @@ type DispensableReagent = { pH: number; }; +type TransferableBeaker = Beaker & { + transferAmounts: number[]; +}; + type Data = { amount: number; chemicals: DispensableReagent[]; - beaker: Beaker; + beaker: TransferableBeaker; }; export const PortableChemMixer = (props) => { diff --git a/tgui/packages/tgui/interfaces/common/BeakerContents.jsx b/tgui/packages/tgui/interfaces/common/BeakerContents.jsx deleted file mode 100644 index b330f956079..00000000000 --- a/tgui/packages/tgui/interfaces/common/BeakerContents.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AnimatedNumber, Box } from '../../components'; - -export const BeakerContents = (props) => { - const { beakerLoaded, beakerContents } = props; - return ( - - {(!beakerLoaded && No beaker loaded.) || - (beakerContents.length === 0 && ( - Beaker is empty. - ))} - {beakerContents.map((chemical) => ( - - - {' units of ' + chemical.name} - - ))} - - ); -}; diff --git a/tgui/packages/tgui/interfaces/common/BeakerDisplay.tsx b/tgui/packages/tgui/interfaces/common/BeakerDisplay.tsx index 5c057755c5c..d567269d907 100644 --- a/tgui/packages/tgui/interfaces/common/BeakerDisplay.tsx +++ b/tgui/packages/tgui/interfaces/common/BeakerDisplay.tsx @@ -15,7 +15,6 @@ type BeakerReagent = { export type Beaker = { maxVolume: number; - transferAmounts: number[]; pH: number; currentVolume: number; contents: BeakerReagent[];