[MIRROR] Chemical Reaction Tester/Debugger 2.0 (#26244)

* 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

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

* 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
🆑
refactor: chemical reaction tester in runtime station has been
remastered from ground up.
/🆑

---------

Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
This commit is contained in:
SkyratBot
2024-02-02 21:00:06 +01:00
committed by GitHub
co-authored by SyncIt21
parent 28648b7164
commit 41ec21dda5
13 changed files with 1728 additions and 1278 deletions
@@ -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 <b>[efficiency*100]%</b>.")
/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 <b>[efficiency * 100]</b>%.")
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, "<span class='warning'>You can't access the maintenance panel while the pod is " \
+ (on ? "active" : (occupant ? "full" : "open")) + "!</span>")
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
@@ -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
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@@ -22,6 +22,10 @@ type DispensableReagent = {
pHCol: string;
};
type TransferableBeaker = Beaker & {
transferAmounts: number[];
};
type Data = {
showpH: BooleanLike;
amount: number;
@@ -31,7 +35,7 @@ type Data = {
recipes: string[];
recordingRecipe: string[];
recipeReagents: string[];
beaker: Beaker;
beaker: TransferableBeaker;
};
export const ChemDispenser = (props) => {
+146 -120
View File
@@ -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 (
<Section
title="Reactions"
buttons={
<Flex>
<Flex.Item color="label">
<AnimatedNumber
value={beaker.pH}
format={(value) => 'pH: ' + round(value, 3)}
/>
</Flex.Item>
<Flex.Item>
<RoundGauge
size={1.6}
value={beaker.pH}
minValue={0}
maxValue={14}
alertAfter={isFlashing}
content={'test'}
format={() => ''}
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],
}}
/>
</Flex.Item>
</Flex>
}
>
{(activeReactions.length === 0 && (
<Box color="label">No active reactions.</Box>
)) || (
<Table>
<Table.Row>
<Table.Cell bold color="label">
Reaction
</Table.Cell>
<Table.Cell bold color="label">
{!highQualityDisplay ? 'Status' : 'Reaction quality'}
</Table.Cell>
<Table.Cell bold color="label">
Target
</Table.Cell>
</Table.Row>
{activeReactions.map((reaction) => (
<Table.Row key="reactions">
<Table.Cell width={'60px'} color={reaction.danger && 'red'}>
{reaction.name}
</Table.Cell>
<Table.Cell width={'100px'} pr={'10px'}>
{!highQualityDisplay ? (
<Icon
name={reaction.danger ? 'exclamation-triangle' : 'spinner'}
color={reaction.danger && 'red'}
spin={!reaction.danger}
ml={2.5}
/>
) : (
<RoundGauge
size={1.3}
value={reaction.quality}
minValue={0}
maxValue={1}
alertAfter={reaction.purityAlert}
content={'test'}
format={(value) => ''}
ml={5}
ranges={{
red: [0, reaction.minPure],
orange: [reaction.minPure, reaction.inverse],
yellow: [reaction.inverse, 0.8],
green: [0.8, 1],
}}
/>
)}
</Table.Cell>
<Table.Cell width={'100px'}>
{highDangerDisplay ? (
<>
{!!reaction.overheat && (
<Icon
name="thermometer-full"
color="red"
style={{ transform: 'scale(1.7)' }}
mr="5px"
/>
)}
<ProgressBar
value={reaction.reactedVol}
minValue={0}
maxValue={reaction.targetVol}
textAlign="center"
width={8}
color={reaction.overheat ? 'red' : 'label'}
>
{reaction.targetVol}u
</ProgressBar>
</>
) : (
<Box color={reaction.danger && 'red'} ml={2}>
{reaction.targetVol}u
</Box>
)}
</Table.Cell>
</Table.Row>
))}
<Table.Row />
</Table>
)}
</Section>
);
};
export const ChemHeater = (props) => {
const { act, data } = useBackend<Data>();
const {
@@ -208,125 +346,13 @@ export const ChemHeater = (props) => {
</Table>
</Section>
{!!isBeakerLoaded && (
<Section
title="Reactions"
buttons={
<Flex>
<Flex.Item color="label">
<AnimatedNumber
value={beaker.pH}
format={(value) => 'pH: ' + round(value, 3)}
/>
</Flex.Item>
<Flex.Item>
<RoundGauge
size={1.6}
value={beaker.pH}
minValue={0}
maxValue={14}
alertAfter={isFlashing}
content={'test'}
format={() => ''}
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],
}}
/>
</Flex.Item>
</Flex>
}
>
{(activeReactions.length === 0 && (
<Box color="label">No active reactions.</Box>
)) || (
<Table>
<Table.Row>
<Table.Cell bold color="label">
Reaction
</Table.Cell>
<Table.Cell bold color="label">
{upgradeLevel < 4 ? 'Status' : 'Reaction quality'}
</Table.Cell>
<Table.Cell bold color="label">
Target
</Table.Cell>
</Table.Row>
{activeReactions.map((reaction) => (
<Table.Row key="reactions">
<Table.Cell width={'60px'} color={reaction.danger && 'red'}>
{reaction.name}
</Table.Cell>
<Table.Cell width={'100px'} pr={'10px'}>
{(upgradeLevel < 4 && (
<Icon
name={
reaction.danger ? 'exclamation-triangle' : 'spinner'
}
color={reaction.danger && 'red'}
spin={!reaction.danger}
ml={2.5}
/>
)) || (
<RoundGauge
size={1.3}
value={reaction.quality}
minValue={0}
maxValue={1}
alertAfter={reaction.purityAlert}
content={'test'}
format={(value) => ''}
ml={5}
ranges={{
red: [0, reaction.minPure],
orange: [reaction.minPure, reaction.inverse],
yellow: [reaction.inverse, 0.8],
green: [0.8, 1],
}}
/>
)}
</Table.Cell>
<Table.Cell width={'100px'}>
{(upgradeLevel > 2 && (
<>
{!!reaction.overheat && (
<Icon
name="thermometer-full"
color="red"
style={{ transform: 'scale(1.7)' }}
mr="5px"
/>
)}
<ProgressBar
value={reaction.reactedVol}
minValue={0}
maxValue={reaction.targetVol}
textAlign="center"
width={8}
color={reaction.overheat ? 'red' : 'label'}
>
{reaction.targetVol}u
</ProgressBar>
</>
)) || (
<Box color={reaction.danger && 'red'} ml={2}>
{reaction.targetVol}u
</Box>
)}
</Table.Cell>
</Table.Row>
))}
<Table.Row />
</Table>
)}
</Section>
<ReactionDisplay
beaker={beaker}
isFlashing={isFlashing}
activeReactions={activeReactions}
highQualityDisplay={upgradeLevel >= 4}
highDangerDisplay={upgradeLevel >= 2}
/>
)}
<BeakerSectionDisplay beaker={beaker} showpH={false} />
</Window.Content>
@@ -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 (
<Window width={450} height={850}>
<Window.Content scrollable>
<Section
title="Controls"
buttons={
<>
<Button
icon={beakerSpawn ? 'power-off' : 'times'}
selected={beakerSpawn}
content={'Spawn beaker'}
onClick={() => act('beakerSpawn')}
/>
<Button
icon={processAll ? 'power-off' : 'times'}
selected={processAll}
content={'All'}
onClick={() => act('all')}
/>
</>
}
>
<LabeledList>
<LabeledList.Item label="Reactions">
<Button icon="plus" onClick={() => act('setTargetList')} />
</LabeledList.Item>
<LabeledList.Item label="Queued">
{(processAll && <Box>All</Box>) || (
<Box>
{queuedReactions.length &&
queuedReactions.map((entry) => entry.name + ', ')}
</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Temp">
{currentTemp}K
<NumberInput
width="65px"
unit="K"
step={10}
stepPixelSize={3}
value={round(targetTemp)}
minValue={0}
maxValue={1000}
onDrag={(e, value) =>
act('temperature', {
target: value,
})
}
/>
<Button
icon={forceTemp ? 'power-off' : 'times'}
selected={forceTemp}
content={'Force'}
onClick={() => act('forceTemp')}
/>
<Button
icon={minTemp ? 'power-off' : 'times'}
selected={minTemp}
content={'MinTemp'}
onClick={() => act('minTemp')}
/>
</LabeledList.Item>
<LabeledList.Item label="Vol multi">
<NumberInput
width="65px"
unit="x"
step={1}
stepPixelSize={3}
value={round(targetVol)}
minValue={1}
maxValue={200}
onDrag={(e, value) =>
act('vol', {
target: value,
})
}
/>
</LabeledList.Item>
<LabeledList.Item label="pH">
{currentpH}
<NumberInput
width="65px"
step={0.1}
stepPixelSize={3}
value={targatpH}
minValue={0}
maxValue={14}
onDrag={(e, value) =>
act('pH', {
target: value,
})
}
/>
<Button
icon={forcepH ? 'power-off' : 'times'}
selected={forcepH}
content={'Force'}
onClick={() => act('forcepH')}
/>
</LabeledList.Item>
<LabeledList.Item label="Index">
{index} of {endIndex}
</LabeledList.Item>
<LabeledList.Item label="Start">
<Button
icon={processing ? 'power-off' : 'times'}
selected={!!processing}
content={'Start'}
onClick={() => act('start')}
/>
<Button
icon={processing ? 'times' : 'power-off'}
color="red"
content={'Stop'}
onClick={() => act('stop')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Recipe edit">
<LabeledList>
<LabeledList.Item
label={editRecipeName ? editRecipeName : 'lookup'}
>
<Button
icon={'flask'}
color="purple"
content={'Select recipe'}
onClick={() => act('setEdit')}
/>
</LabeledList.Item>
{!!editRecipe && (
<>
<LabeledList.Item label="is_cold_recipe">
<Button
icon={editRecipeCold ? 'smile' : 'times'}
color={editRecipeCold ? 'green' : 'red'}
content={'Cold?'}
onClick={() =>
act('updateVar', {
type: entry.name,
target: value,
})
}
/>
</LabeledList.Item>
{editRecipe.map((entry) => (
<LabeledList.Item label={entry.name} key={entry.name}>
<NumberInput
width="65px"
step={1}
stepPixelSize={3}
value={entry.var}
minValue={-9999}
maxValue={9999}
onDrag={(e, value) =>
act('updateVar', {
type: entry.name,
target: value,
})
}
/>
</LabeledList.Item>
))}
</>
)}
<LabeledList.Item label="Export">
<Button
icon={'save'}
color="green"
content={'export'}
onClick={() => act('export')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
<Section
title="Reactions"
buttons={
<Flex>
<Flex.Item color="label">
<AnimatedNumber
value={currentpH}
format={(value) => 'pH: ' + round(value, 3)}
/>
</Flex.Item>
<Flex.Item>
<RoundGauge
size={1.6}
value={currentpH}
minValue={0}
maxValue={14}
alertAfter={isFlashing}
content={'test'}
format={() => ''}
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],
}}
/>
</Flex.Item>
</Flex>
}
>
{(activeReactions.length === 0 && (
<Box color="label">No active reactions.</Box>
)) || (
<Table>
<Table.Row>
<Table.Cell bold color="label">
Reaction
</Table.Cell>
<Table.Cell bold color="label">
{'Reaction quality'}
</Table.Cell>
<Table.Cell bold color="label">
Target
</Table.Cell>
</Table.Row>
{activeReactions &&
activeReactions.map((reaction) => (
<Table.Row key="reactions">
<Table.Cell width={'60px'} color={reaction.danger && 'red'}>
{reaction.name}
</Table.Cell>
<Table.Cell width={'100px'} pr={'10px'}>
<RoundGauge
size={1.3}
value={reaction.quality}
minValue={0}
maxValue={1}
alertAfter={reaction.purityAlert}
content={'test'}
format={() => ''}
ml={5}
ranges={{
red: [0, reaction.minPure],
orange: [reaction.minPure, reaction.inverse],
yellow: [reaction.inverse, 0.8],
green: [0.8, 1],
}}
/>
</Table.Cell>
<Table.Cell width={'70px'}>
<ProgressBar
value={reaction.reactedVol}
minValue={0}
maxValue={reaction.targetVol}
textAlign={'center'}
icon={reaction.overheat && 'thermometer-full'}
width={7}
color={reaction.overheat ? 'red' : 'label'}
>
{reaction.targetVol}u
</ProgressBar>
</Table.Cell>
</Table.Row>
))}
<Table.Row />
</Table>
)}
</Section>
<Section
title="Chamber"
buttons={<Box>{isActive ? 'Reacting' : 'Waiting'}</Box>}
>
{(chamberContents.length && (
<BeakerContents beakerLoaded beakerContents={chamberContents} />
)) || <Box>Nothing</Box>}
</Section>
</Window.Content>
</Window>
);
};
@@ -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<Data>();
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 (
<Window width={500} height={600}>
<Window.Content scrollable>
<Section title="Controls">
<Tabs>
<Tabs.Tab
key={'Environment'}
selected={controlState === 'Environment'}
onClick={() => setControlState('Environment')}
>
Environment
</Tabs.Tab>
<Tabs.Tab
key={'Reactions'}
selected={controlState === 'Reactions'}
onClick={() => setControlState('Reactions')}
>
Reactions
</Tabs.Tab>
<Tabs.Tab
key={'Editing'}
selected={controlState === 'Editing'}
onClick={() => setControlState('Editing')}
>
Edit Reactions
</Tabs.Tab>
</Tabs>
{controlState === 'Environment' && (
<Section>
<Stack vertical={false}>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Temperature">
<NumberInput
width="65px"
step={1}
stepPixelSize={3}
value={forced_temp}
minValue={0}
maxValue={1000}
onDrag={(e, value) =>
act('forced_temp', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item>
<LabeledList>
<LabeledList.Item
label={
<Box
style={{
transform: 'translate(0%, -50%)',
}}
>
Temp Mode:
</Box>
}
>
<Dropdown
width="100%"
selected={TEMP_MODES[temp_mode]}
options={TEMP_MODES}
onSelected={(value) =>
act('temp_mode', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
<Stack vertical={false} mt="10px">
<Stack.Item>
<LabeledList>
<LabeledList.Item label={<Box width="82px">PH:</Box>}>
<NumberInput
width="65px"
step={1}
stepPixelSize={3}
value={forced_ph}
minValue={0}
maxValue={14}
onDrag={(e, value) =>
act('forced_ph', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item ml="0px">
<LabeledList>
<LabeledList.Item label="Force Ph">
<Button.Checkbox
checked={use_forced_ph}
onClick={() => act('toggle_forced_ph')}
>
{use_forced_ph ? 'Disable' : 'Enable'}
</Button.Checkbox>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
<Stack vertical={false} mt="10px">
<Stack.Item>
<LabeledList>
<LabeledList.Item label={<Box width="82px">Purity:</Box>}>
<NumberInput
width="65px"
step={0.01}
stepPixelSize={3}
value={forced_purity}
minValue={0}
maxValue={1}
onDrag={(e, value) =>
act('forced_purity', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item ml="10px">
<LabeledList>
<LabeledList.Item label="Force Purity">
<Button.Checkbox
checked={use_forced_purity}
onClick={() => act('toggle_forced_purity')}
>
{use_forced_purity ? 'Disable' : 'Enable'}
</Button.Checkbox>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
<Stack vertical={false} mt="10px">
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Volume Mulx">
<NumberInput
width="65px"
step={1}
stepPixelSize={3}
value={volume_multiplier}
minValue={1}
maxValue={1000}
unit="x"
onDrag={(e, value) =>
act('volume_multiplier', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
</Section>
)}
{controlState === 'Reactions' && (
<Section>
<Stack vertical>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Reagent">
<Button
color="green"
onClick={() => act('pick_reaction')}
>
Select Reaction
</Button>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item label="Reaction">
{current_reaction_name}
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item
label={
<Box
style={{
transform: 'translate(0%, -50%)',
}}
>
Direction:
</Box>
}
>
<Dropdown
width="35%"
selected={REACTION_MODES[current_reaction_mode]}
options={REACTION_MODES}
disabled={current_reaction_name === 'N/A'}
onSelected={(value) =>
act('reaction_mode', {
target: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item label={<Box width="60px">Process:</Box>}>
<Button
color="green"
icon="play"
disabled={isReacting || current_reaction_name === 'N/A'}
onClick={() => act('start_reaction')}
>
Play
</Button>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
</Section>
)}
{controlState === 'Editing' && (
<Section>
<Stack vertical>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Reaction">
<Button
color="green"
icon="flask"
onClick={() => act('edit_reaction')}
>
{editReaction?.name || 'Edit Reaction'}
</Button>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item
label={
<Box
style={{
transform: 'translate(0%, -50%)',
width: '57px',
}}
>
Param:
</Box>
}
>
<Dropdown
width="40%"
selected={editReaction?.editVar || REACTION_VARS[1]}
options={REACTION_VARS}
onSelected={(value) =>
act('edit_var', { target: value })
}
disabled={editReaction === null}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item label={<Box width="57px">Value:</Box>}>
<NumberInput
width="65px"
step={0.1}
stepPixelSize={3}
value={editReaction?.editValue || 0}
minValue={0}
maxValue={1000}
disabled={editReaction === null}
onDrag={(e, value) =>
act('edit_value', {
target: value,
})
}
/>
<Button
color="green"
icon="sync"
tooltip="Reset Value"
disabled={editReaction === null}
onClick={() => act('reset_value')}
/>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item mt="20px">
<LabeledList>
<LabeledList.Item label={<Box width="57px">Export:</Box>}>
<Button
color="green"
icon="save"
onClick={() => act('export')}
disabled={editReaction === null}
>
Export
</Button>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
</Section>
)}
</Section>
{beaker && (
<Section title="Variables">
<LabeledList>
<LabeledList.Item label="Temperature">
{beaker.currentTemp}
</LabeledList.Item>
<LabeledList.Item label="Purity">
{beaker.purity}
</LabeledList.Item>
</LabeledList>
</Section>
)}
{beaker && (
<ReactionDisplay
beaker={beaker}
isFlashing={isFlashing}
activeReactions={activeReactions}
highQualityDisplay
highDangerDisplay
/>
)}
<BeakerSectionDisplay
title_label="Internal Buffer"
beaker={beaker}
showpH={false}
/>
</Window.Content>
</Window>
);
};
-128
View File
@@ -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 (
<Window width={400} height={550}>
<Window.Content scrollable>
<CryoContent />
</Window.Content>
</Window>
);
};
const CryoContent = (props) => {
const { act, data } = useBackend();
return (
<>
<Section title="Occupant">
<LabeledList>
<LabeledList.Item label="Occupant">
{data.occupant.name || 'No Occupant'}
</LabeledList.Item>
{!!data.hasOccupant && (
<>
<LabeledList.Item label="State" color={data.occupant.statstate}>
{data.occupant.stat}
</LabeledList.Item>
<LabeledList.Item
label="Temperature"
color={data.occupant.temperaturestatus}
>
<AnimatedNumber value={data.occupant.bodyTemperature} />
{' K'}
</LabeledList.Item>
<LabeledList.Item label="Health">
<ProgressBar
value={data.occupant.health / data.occupant.maxHealth}
color={data.occupant.health > 0 ? 'good' : 'average'}
>
<AnimatedNumber value={data.occupant.health} />
</ProgressBar>
</LabeledList.Item>
{damageTypes.map((damageType) => (
<LabeledList.Item key={damageType.id} label={damageType.label}>
<ProgressBar value={data.occupant[damageType.type] / 100}>
<AnimatedNumber value={data.occupant[damageType.type]} />
</ProgressBar>
</LabeledList.Item>
))}
</>
)}
</LabeledList>
</Section>
<Section title="Cell">
<LabeledList>
<LabeledList.Item label="Power">
<Button
icon={data.isOperating ? 'power-off' : 'times'}
disabled={data.isOpen}
onClick={() => act('power')}
color={data.isOperating && 'green'}
>
{data.isOperating ? 'On' : 'Off'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Temperature">
<AnimatedNumber value={data.cellTemperature} /> K
</LabeledList.Item>
<LabeledList.Item label="Door">
<Button
icon={data.isOpen ? 'unlock' : 'lock'}
onClick={() => act('door')}
content={data.isOpen ? 'Open' : 'Closed'}
/>
<Button
icon={data.autoEject ? 'sign-out-alt' : 'sign-in-alt'}
onClick={() => act('autoeject')}
content={data.autoEject ? 'Auto' : 'Manual'}
/>
</LabeledList.Item>
</LabeledList>
</Section>
<Section
title="Beaker"
buttons={
<Button
icon="eject"
disabled={!data.isBeakerLoaded}
onClick={() => act('ejectbeaker')}
content="Eject"
/>
}
>
<BeakerContents
beakerLoaded={data.isBeakerLoaded}
beakerContents={data.beakerContents}
/>
</Section>
</>
);
};
+150
View File
@@ -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<Data>();
const { occupant, isOperating, isOpen } = data;
return (
<Window width={400} height={550}>
<Window.Content scrollable>
<Section title="Occupant">
<LabeledList>
<LabeledList.Item label="Occupant">
{occupant?.name || 'No Occupant'}
</LabeledList.Item>
{!!occupant && (
<>
<LabeledList.Item
label="State"
color={stat_to_color[occupant.stat]}
>
{occupant.stat}
</LabeledList.Item>
<LabeledList.Item
label="Temperature"
color={occupant.bodyTemperature < data.T0C ? 'good' : 'bad'} // Green if the mob can actually be healed by cryoxadone.
>
<AnimatedNumber value={round(occupant.bodyTemperature, 0)} />
{' K'}
</LabeledList.Item>
<LabeledList.Item label="Health">
<ProgressBar
value={round(occupant.health / occupant.maxHealth, 2)}
color={occupant.health > 0 ? 'good' : 'average'}
>
<AnimatedNumber value={round(occupant.health, 0)} />
</ProgressBar>
</LabeledList.Item>
{damageTypes.map((damageType) => (
<LabeledList.Item
key={damageType.type}
label={damageType.label}
>
<ProgressBar
value={round(data.occupant[damageType.type] / 100, 2)}
>
<AnimatedNumber
value={round(data.occupant[damageType.type], 0)}
/>
</ProgressBar>
</LabeledList.Item>
))}
</>
)}
</LabeledList>
</Section>
<Section title="Cell">
<LabeledList>
<LabeledList.Item label="Power">
<Button
icon={isOperating ? 'power-off' : 'times'}
disabled={isOpen}
onClick={() => act('power')}
color={isOperating && 'green'}
>
{isOperating ? 'On' : 'Off'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Temperature">
<AnimatedNumber value={round(data.cellTemperature, 0)} /> K
</LabeledList.Item>
<LabeledList.Item label="Door">
<Button
icon={isOpen ? 'unlock' : 'lock'}
onClick={() => act('door')}
>
{isOpen ? 'Open' : 'Closed'}
</Button>
<Button
icon={data.autoEject ? 'sign-out-alt' : 'sign-in-alt'}
onClick={() => act('autoeject')}
>
{data.autoEject ? 'Auto' : 'Manual'}
</Button>
</LabeledList.Item>
</LabeledList>
</Section>
<BeakerSectionDisplay beaker={data.beaker} showpH={false} />
</Window.Content>
</Window>
);
};
@@ -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) => {
@@ -1,19 +0,0 @@
import { AnimatedNumber, Box } from '../../components';
export const BeakerContents = (props) => {
const { beakerLoaded, beakerContents } = props;
return (
<Box>
{(!beakerLoaded && <Box color="label">No beaker loaded.</Box>) ||
(beakerContents.length === 0 && (
<Box color="label">Beaker is empty.</Box>
))}
{beakerContents.map((chemical) => (
<Box key={chemical.name} color="label">
<AnimatedNumber initial={0} value={chemical.volume} />
{' units of ' + chemical.name}
</Box>
))}
</Box>
);
};
@@ -15,7 +15,6 @@ type BeakerReagent = {
export type Beaker = {
maxVolume: number;
transferAmounts: number[];
pH: number;
currentVolume: number;
contents: BeakerReagent[];