General maintainence for HPLC (#81613)

## About The Pull Request

Moves around code for portable chem mixer & chem heater. No functional
changes, As for the HPLC

**1. Qol**
- Adds examines & screentips for
    1) Screwdriver, crowbar & wrench acts
    2) Inserting & replacing input/output beakers
    3) Alt click ejecting input/output beakers
- Balloon alerts replaces chat messages to reduce spam.
- HPLC will now display the status of each reagent if it can be purified
or not before starting the refining process thus saving you the headache
of pressing the start button only to waste power & time & get no work
done

**2. Code Improvements**
- Moved screwdriver, wrench & crowbar acts out of `attackby()` into
their respective tool act PROCS
- Merged procs `calculate_largest_mass()` & ` calculate_smallest_mass()`
procs into a single proc `calculate_mass()` with a boolean var to decide
if we want the smallest or largest mass reagent
  - Computes estimated time only when UI changes & not every ui update
 
**3. Fixes**
 - HPLC won't accept hologram or abstract items
- HPLC now displays off icon state & pauses processing when the machine
is either opened, powered off, unanchored or broken
- HPLC will ignore reagents that are either already at max purity or are
inverse thus lowering eta & work done to purify your remaining reagents

**4. Refactor**
- Converted UI to typescript, moved vars such as `in_range` & `color` to
the client side to reduce data sent.
This commit is contained in:
SyncIt21
2024-03-10 22:50:23 +05:30
committed by GitHub
parent 3c40e04de2
commit 352c8bdbfb
5 changed files with 923 additions and 769 deletions
@@ -25,15 +25,14 @@
create_reagents(200, NO_REACT)
register_context()
/obj/machinery/chem_heater/on_deconstruction(disassembled)
beaker?.forceMove(drop_location())
/obj/machinery/chem_heater/Destroy()
if(beaker)
UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
QDEL_NULL(beaker)
return ..()
/obj/machinery/chem_heater/on_deconstruction(disassembled)
beaker?.forceMove(drop_location())
/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))
@@ -60,10 +59,74 @@
return NONE
/obj/machinery/chem_heater/examine(mob/user)
. = ..()
if(in_range(user, src) || isobserver(user))
. += span_notice("The status display reads: Heating reagents at <b>[heater_coefficient * 1000]%</b> speed.")
if(!QDELETED(beaker))
. += span_notice("It has a beaker of [beaker.reagents.total_volume] units capacity.")
if(beaker.reagents.is_reacting)
. += span_notice("Its contents are currently reacting.")
else
. += span_warning("There is no beaker inserted.")
. += span_notice("Its heating is turned [on ? "On" : "Off"].")
. += span_notice("The status display reads: Heating reagents at <b>[heater_coefficient * 1000]%</b> speed.")
if(panel_open)
. += span_notice("Its panel is open and can now be [EXAMINE_HINT("pried")] apart.")
else
. += span_notice("Its panel can be [EXAMINE_HINT("pried")] open")
/obj/machinery/chem_heater/update_icon_state()
icon_state = "[base_icon_state][beaker ? 1 : 0]b"
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/RefreshParts()
. = ..()
heater_coefficient = 0.1
for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
heater_coefficient *= micro_laser.tier
/obj/machinery/chem_heater/item_interaction(mob/living/user, obj/item/held_item, list/modifiers, is_right_clicking)
if((held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1))
return ..()
if(QDELETED(beaker))
if(istype(held_item, /obj/item/reagent_containers/dropper) || istype(held_item, /obj/item/reagent_containers/syringe))
var/obj/item/reagent_containers/injector = held_item
injector.afterattack(beaker, user, proximity_flag = TRUE)
return ITEM_INTERACT_SUCCESS
if(is_reagent_container(held_item) && held_item.is_open_container())
if(replace_beaker(user, held_item))
ui_interact(user)
balloon_alert(user, "beaker added")
return ITEM_INTERACT_SUCCESS
return ..()
/obj/machinery/chem_heater/wrench_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/screwdriver_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_deconstruction_screwdriver(user, "mixer0b", "[base_icon_state][beaker ? 1 : 0]b", tool))
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/crowbar_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_deconstruction_crowbar(tool))
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
@@ -73,13 +136,6 @@
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)
@@ -109,12 +165,6 @@
return TRUE
/obj/machinery/chem_heater/RefreshParts()
. = ..()
heater_coefficient = 0.1
for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
heater_coefficient *= micro_laser.tier
/**
* Heats the reagents of the currently inserted beaker only if machine is on & beaker has some reagents inside
* Arguments
@@ -142,23 +192,6 @@
for(var/datum/tgui/ui in src.open_uis)
ui.send_update()
/obj/machinery/chem_heater/examine(mob/user)
. = ..()
if(in_range(user, src) || isobserver(user))
. += span_notice("The status display reads: Heating reagents at <b>[heater_coefficient * 1000]%</b> speed.")
if(!QDELETED(beaker))
. += span_notice("It has a beaker of [beaker.reagents.total_volume] units capacity.")
if(beaker.reagents.is_reacting)
. += span_notice("Its contents are currently reacting.")
else
. += span_warning("There is no beaker inserted.")
. += span_notice("Its heating is turned [on ? "On" : "Off"].")
. += span_notice("The status display reads: Heating reagents at <b>[heater_coefficient * 1000]%</b> speed.")
if(panel_open)
. += span_notice("Its panel is open and can now be [EXAMINE_HINT("pried")] apart.")
else
. += span_notice("Its panel can be [EXAMINE_HINT("pried")] open")
/obj/machinery/chem_heater/process(seconds_per_tick)
//is_reacting is handled in reaction_step()
if(QDELETED(beaker) || beaker.reagents.is_reacting)
@@ -172,39 +205,6 @@
for(var/datum/tgui/ui in src.open_uis)
ui.send_update()
/obj/machinery/chem_heater/wrench_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/screwdriver_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_deconstruction_screwdriver(user, "mixer0b", "[base_icon_state][beaker ? 1 : 0]b", tool))
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/crowbar_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(default_deconstruction_crowbar(tool))
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_heater/attackby(obj/item/held_item, mob/user, params)
if((held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1))
return ..()
if(beaker)
if(istype(held_item, /obj/item/reagent_containers/dropper) || istype(held_item, /obj/item/reagent_containers/syringe))
var/obj/item/reagent_containers/injector = held_item
injector.afterattack(beaker, user, proximity_flag = TRUE)
return TRUE
if(is_reagent_container(held_item) && held_item.is_open_container())
if(replace_beaker(user, held_item))
ui_interact(user)
balloon_alert(user, "beaker added!")
return TRUE
return ..()
/obj/machinery/chem_heater/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
@@ -1,21 +1,16 @@
#define BEAKER1 1
#define BEAKER2 2
/obj/machinery/chem_mass_spec
name = "High-performance liquid chromatography machine"
desc = {"This machine can separate reagents based on charge, meaning it can clean reagents of some of their impurities, unlike the Chem Master 3000.
By selecting a range in the mass spectrograph certain reagents will be transferred from one beaker to another, which will clean it of any impurities up to a certain amount.
This will not clean any inverted reagents. Inverted reagents will still be correctly detected and displayed on the scanner, however.
\nLeft click with a beaker to add it to the input slot, Right click with a beaker to add it to the output slot. Alt + left/right click can let you quickly remove the corresponding beaker."}
density = TRUE
layer = BELOW_OBJ_LAYER
desc = "Allows you to purify reagents & seperate out inverse reagents"
icon = 'icons/obj/medical/chemical.dmi'
icon_state = "HPLC"
base_icon_state = "HPLC"
density = TRUE
interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_REQUIRES_ANCHORED
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION * 0.2
resistance_flags = FIRE_PROOF | ACID_PROOF
processing_flags = START_PROCESSING_MANUALLY
circuit = /obj/item/circuitboard/machine/chem_mass_spec
///If we're processing reagents or not
var/processing_reagents = FALSE
///Time we started processing + the delay
@@ -37,157 +32,244 @@ This will not clean any inverted reagents. Inverted reagents will still be corre
/obj/machinery/chem_mass_spec/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_DO_NOT_SPLASH, INNATE_TRAIT)
if(mapload)
beaker2 = new /obj/item/reagent_containers/cup/beaker/large(src)
AddElement( \
/datum/element/contextual_screentip_bare_hands, \
lmb_text = "Add input beaker", \
rmb_text = "Add output beaker", \
)
register_context()
/obj/machinery/chem_mass_spec/Destroy()
QDEL_NULL(beaker1)
QDEL_NULL(beaker2)
return ..()
/obj/machinery/chem_mass_spec/RefreshParts()
. = ..()
cms_coefficient = 1
for(var/datum/stock_part/micro_laser/laser in component_parts)
cms_coefficient /= laser.tier
/obj/machinery/chem_mass_spec/on_deconstruction(disassembled)
if(beaker1)
beaker1.forceMove(drop_location())
beaker1 = null
if(beaker2)
beaker2.forceMove(drop_location())
beaker2 = null
var/location = drop_location()
beaker1?.forceMove(location)
beaker2?.forceMove(location)
/obj/machinery/chem_mass_spec/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = NONE
if(!QDELETED(beaker1))
context[SCREENTIP_CONTEXT_ALT_LMB] = "Eject input beaker"
. = CONTEXTUAL_SCREENTIP_SET
if(!QDELETED(beaker2))
context[SCREENTIP_CONTEXT_ALT_RMB] = "Eject output beaker"
. = CONTEXTUAL_SCREENTIP_SET
if(isnull(held_item) || (held_item.item_flags & ABSTRACT) || (held_item.flags_1 & HOLOGRAM_1))
return
if(is_reagent_container(held_item))
if(QDELETED(beaker1))
context[SCREENTIP_CONTEXT_LMB] = "Insert input beaker"
else
context[SCREENTIP_CONTEXT_LMB] = "Replace input beaker"
if(QDELETED(beaker2))
context[SCREENTIP_CONTEXT_RMB] = "Insert output beaker"
else
context[SCREENTIP_CONTEXT_RMB] = "Replace output beaker"
return CONTEXTUAL_SCREENTIP_SET
if(held_item.tool_behaviour == TOOL_WRENCH)
context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchor"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "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
/obj/machinery/chem_mass_spec/examine(mob/user)
. = ..()
if(!QDELETED(beaker1))
. += span_notice("Input beaker of [beaker1.reagents.maximum_volume]u capacity is inserted.")
. += span_notice("Its Input beaker Can be ejected with [EXAMINE_HINT("LMB Alt")] click.")
else
. += span_warning("Its missing an input beaker. insert with [EXAMINE_HINT("Left Click")].")
if(!QDELETED(beaker2))
. += span_notice("Output beaker of [beaker2.reagents.maximum_volume]u capacity is inserted.")
. += span_notice("Its Output beaker can be ejected with [EXAMINE_HINT("RMB Alt")] click.")
else
. += span_warning("Its missing an output beaker, insert with [EXAMINE_HINT("Right Click")].")
if(anchored)
. += span_notice("Its [EXAMINE_HINT("anchored")] in place.")
else
. += span_warning("Needs to be [EXAMINE_HINT("wrenched")] to use.")
. += span_notice("Its maintainence panel can be [EXAMINE_HINT("screwed")] [panel_open ? "closed" : "open"].")
if(panel_open)
. += span_notice("It can be [EXAMINE_HINT("pried")] apart.")
/obj/machinery/chem_mass_spec/update_overlays()
. = ..()
if(panel_open)
. += mutable_appearance(icon, "[base_icon_state]_panel-o")
/obj/machinery/chem_mass_spec/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
return ITEM_INTERACT_SUCCESS
/* beaker swapping/attack code */
/obj/machinery/chem_mass_spec/attackby(obj/item/item, mob/user, params)
if(processing_reagents)
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
return ..()
if(default_deconstruction_screwdriver(user, icon_state, icon_state, item))
update_appearance()
return
if(is_reagent_container(item) && !(item.item_flags & ABSTRACT) && item.is_open_container())
var/obj/item/reagent_containers/beaker = item
. = TRUE //no afterattack
if(!user.transferItemToLoc(beaker, src))
return
replace_beaker(user, BEAKER1, beaker)
to_chat(user, span_notice("You add [beaker] to [src]."))
update_appearance()
ui_interact(user)
return
..()
/obj/machinery/chem_mass_spec/attackby_secondary(obj/item/item, mob/user, params)
. = ..()
if(processing_reagents)
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
return
if(default_deconstruction_crowbar(item))
return
if(is_reagent_container(item) && !(item.item_flags & ABSTRACT) && item.is_open_container())
var/obj/item/reagent_containers/beaker = item
if(!user.transferItemToLoc(beaker, src))
return
replace_beaker(user, BEAKER2, beaker)
to_chat(user, span_notice("You add [beaker] to [src]."))
ui_interact(user)
. = SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
update_appearance()
/obj/machinery/chem_mass_spec/AltClick(mob/living/user)
. = ..()
if(processing_reagents)
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
return
if(!can_interact(user) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return ..()
replace_beaker(user, BEAKER1)
/obj/machinery/chem_mass_spec/alt_click_secondary(mob/living/user)
. = ..()
if(processing_reagents)
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
return
if(!can_interact(user) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return
replace_beaker(user, BEAKER2)
///Gee how come you get two beakers?
/*
* Similar to other replace beaker procs, except now there are two of them!
* When passed a beaker along with a position define it will swap a beaker in that slot (if there is one) with the beaker the machine is bonked with
*
* arguments:
* * user - The one bonking the machine
* * target beaker - the define (BEAKER1/BEAKER2) of what position to replace
* * new beaker - the new beaker to add/replace the slot with
*/
/obj/machinery/chem_mass_spec/proc/replace_beaker(mob/living/user, target_beaker, obj/item/reagent_containers/new_beaker)
if(!user)
return FALSE
switch(target_beaker)
if(BEAKER1)
if(beaker1)
try_put_in_hand(beaker1, user)
beaker1 = null
beaker1 = new_beaker
lower_mass_range = calculate_smallest_mass()
upper_mass_range = calculate_largest_mass()
if(BEAKER2)
if(beaker2)
try_put_in_hand(beaker2, user)
beaker2 = null
beaker2 = new_beaker
update_appearance()
return TRUE
/* Icon code */
/obj/machinery/chem_mass_spec/update_icon_state()
if(powered())
icon_state = "HPLC_on"
else
icon_state = "HPLC"
return ..()
/obj/machinery/chem_mass_spec/update_overlays()
. = ..()
if(beaker1)
if(!QDELETED(beaker1))
. += "HPLC_beaker1"
if(beaker2)
if(!QDELETED(beaker2))
. += "HPLC_beaker2"
if(powered())
if(is_operational && !panel_open && anchored && !(machine_stat & (BROKEN | NOPOWER)))
if(processing_reagents)
. += "HPLC_graph_active"
else if (length(beaker1?.reagents.reagent_list))
. += "HPLC_graph_idle"
/* UI Code */
/obj/machinery/chem_mass_spec/update_icon_state()
if(is_operational && !panel_open && anchored && !(machine_stat & (BROKEN | NOPOWER)))
icon_state = "HPLC_on"
else
icon_state = "HPLC"
return ..()
/obj/machinery/chem_mass_spec/Exited(atom/movable/gone, direction)
. = ..()
if(gone == beaker1)
beaker1 = null
if(gone == beaker2)
beaker2 = null
/obj/machinery/chem_mass_spec/RefreshParts()
. = ..()
cms_coefficient = 1
for(var/datum/stock_part/micro_laser/laser in component_parts)
cms_coefficient /= laser.tier
/obj/machinery/chem_mass_spec/item_interaction(mob/living/user, obj/item/item, list/modifiers, is_right_clicking)
if((item.item_flags & ABSTRACT) || (item.flags_1 & HOLOGRAM_1) || !can_interact(user) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
return ..()
if(is_reagent_container(item) && item.is_open_container())
if(processing_reagents)
balloon_alert(user, "still processing!")
return ITEM_INTERACT_BLOCKING
var/obj/item/reagent_containers/beaker = item
if(!user.transferItemToLoc(beaker, src))
return ITEM_INTERACT_BLOCKING
replace_beaker(user, !is_right_clicking, beaker)
to_chat(user, span_notice("You add [beaker] to [is_right_clicking ? "output" : "input"] slot."))
update_appearance()
ui_interact(user)
return ITEM_INTERACT_SUCCESS
return ..()
/obj/machinery/chem_mass_spec/wrench_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(processing_reagents)
balloon_alert(user, "still processing!")
return .
if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN)
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_mass_spec/screwdriver_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(processing_reagents)
balloon_alert(user, "still processing!")
return .
if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool))
update_appearance()
return ITEM_INTERACT_SUCCESS
/obj/machinery/chem_mass_spec/crowbar_act(mob/living/user, obj/item/tool)
. = ITEM_INTERACT_BLOCKING
if(processing_reagents)
balloon_alert(user, "still processing!")
return .
if(default_deconstruction_crowbar(tool))
return ITEM_INTERACT_SUCCESS
/**
* Computes either the lightest or heaviest reagent in the input beaker
* Arguments
*
* * smallest - TRUE to find lightest reagent, FALSE to find heaviest reagent
*/
/obj/machinery/chem_mass_spec/proc/calculate_mass(smallest = TRUE)
PRIVATE_PROC(TRUE)
SHOULD_BE_PURE(TRUE)
if(QDELETED(beaker1))
return 0
var/result = 0
for(var/datum/reagent/reagent as anything in beaker1?.reagents.reagent_list)
var/datum/reagent/target = reagent
if(!istype(reagent, /datum/reagent/inverse) && (reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem))
target = GLOB.chemical_reagents_list[reagent.inverse_chem]
if(!result)
result = target.mass
else
result = smallest ? min(result, reagent.mass) : max(result, reagent.mass)
return smallest ? FLOOR(result, 50) : CEILING(result, 50)
/*
* Replaces a beaker in the machine, either input or output
* Arguments
*
* * user - The one bonking the machine
* * target beaker - the target beaker we are trying to replace
* * new beaker - the new beaker to add/replace the slot with
*/
/obj/machinery/chem_mass_spec/proc/replace_beaker(mob/living/user, is_input, obj/item/reagent_containers/new_beaker)
PRIVATE_PROC(TRUE)
if(is_input) //replace input beaker
if(!QDELETED(beaker1))
try_put_in_hand(beaker1, user)
beaker1 = new_beaker
lower_mass_range = calculate_mass(smallest = TRUE)
upper_mass_range = calculate_mass(smallest = FALSE)
estimate_time()
else //replace output beaker
if(!QDELETED(beaker2))
try_put_in_hand(beaker2, user)
beaker2 = new_beaker
update_appearance()
///Computes time to purity reagents
/obj/machinery/chem_mass_spec/proc/estimate_time()
PRIVATE_PROC(TRUE)
delay_time = 0
if(QDELETED(beaker1))
return
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
//we don't bother about impure chems
if(istype(reagent, /datum/reagent/inverse) || (reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem))
continue
//out of our selected range
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
continue
//already at max purity
if((initial(reagent.purity) - reagent.purity) <= 0)
continue
///Roughly 10 - 30s?
delay_time += (((reagent.mass * reagent.volume) + (reagent.mass * reagent.get_inverse_purity() * 0.1)) * 0.0035) + 10
delay_time *= cms_coefficient
/obj/machinery/chem_mass_spec/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
@@ -196,209 +278,214 @@ This will not clean any inverted reagents. Inverted reagents will still be corre
ui.open()
/obj/machinery/chem_mass_spec/ui_data(mob/user)
var/data = list()
data["graphLowerRange"] = 0
data["lowerRange"] = lower_mass_range
data["upperRange"] = upper_mass_range
data["processing"] = processing_reagents
data["log"] = log
data["beaker1"] = beaker1 ? TRUE : FALSE
data["beaker2"] = beaker2 ? TRUE : FALSE
if(processing_reagents)
data["eta"] = delay_time - progress_time
else
data["eta"] = estimate_time()
. = list()
.["lowerRange"] = lower_mass_range
.["upperRange"] = upper_mass_range
.["processing"] = processing_reagents
.["eta"] = delay_time - progress_time
.["peakHeight"] = 0
var/beakerContents[0]
if(beaker1 && beaker1.reagents)
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
var/in_range = TRUE
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
in_range = FALSE
beakerContents.Add(list(list("name" = inverse_reagent.name, "volume" = round(reagent.volume, 0.01), "mass" = inverse_reagent.mass, "purity" = round(reagent.get_inverse_purity(), 0.000001)*100, "selected" = in_range, "color" = "#b60046", "type" = "Inverted")))
data["peakHeight"] = max(data["peakHeight"], reagent.volume)
continue
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
in_range = FALSE
///We want to be sure that the impure chem appears after the parent chem in the list so that it always overshadows pure reagents
beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume, 0.01), "mass" = reagent.mass, "purity" = round(reagent.purity, 0.000001)*100, "selected" = in_range, "color" = "#3cf096", "type" = "Clean")))
data["peakHeight"] = max(data["peakHeight"], reagent.volume)
//input reagents
var/list/beaker1Data = null
if(!QDELETED(beaker1))
beaker1Data = list()
var/datum/reagents/beaker_1_reagents = beaker1.reagents
beaker1Data["currentVolume"] = beaker_1_reagents.total_volume
beaker1Data["maxVolume"] = beaker_1_reagents.maximum_volume
var/list/beakerContents = list()
for(var/datum/reagent/reagent as anything in beaker_1_reagents.reagent_list)
var/log = ""
var/datum/reagent/target = reagent
var/purity = target.purity
var/is_inverse = FALSE
data["beaker1CurrentVolume"] = beaker1.reagents.total_volume
data["beaker1MaxVolume"] = beaker1.reagents.maximum_volume
data["beaker1Contents"] = beakerContents
data["graphUpperRange"] = calculate_largest_mass() //+10 because of the range on the peak
if(istype(reagent, /datum/reagent/inverse))
log = "Too impure to use" //we don't bother about impure chems
is_inverse = TRUE
else if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
purity = target.get_inverse_purity()
target = GLOB.chemical_reagents_list[reagent.inverse_chem]
log = "Too impure to use" //we don't bother about impure chems
is_inverse = TRUE
else
var/initial_purity = initial(reagent.purity)
if((initial_purity - reagent.purity) <= 0) //already at max purity
log = "Cannot purify above [round(initial_purity * 100)]%"
else
log = "Ready"
beakerContents = list()
if(beaker2 && beaker2.reagents)
for(var/datum/reagent/reagent in beaker2.reagents.reagent_list)
///Normal stuff
beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume, 0.01), "mass" = reagent.mass, "purity" = round(reagent.purity, 0.000001)*100, "color" = "#3cf096", "type" = "Clean", log = log[reagent.type])))
data["beaker2CurrentVolume"] = beaker2.reagents.total_volume
data["beaker2MaxVolume"] = beaker2.reagents.maximum_volume
data["beaker2Contents"] = beakerContents
beakerContents += list(list(
"name" = target.name,
"volume" = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING),
"mass" = target.mass,
"purity" = round(purity * 100),
"type" = is_inverse ? "Inverted" : "Clean",
"log" = log
))
.["peakHeight"] = max(.["peakHeight"], reagent.volume)
beaker1Data["contents"] = beakerContents
.["beaker1"] = beaker1Data
return data
//+10 because of the range on the peak
.["graphUpperRange"] = calculate_mass(smallest = FALSE)
/obj/machinery/chem_mass_spec/ui_act(action, params)
//output reagents
var/list/beaker2Data = null
if(!QDELETED(beaker2))
beaker2Data = list()
var/datum/reagents/beaker_2_reagents = beaker2.reagents
beaker2Data["currentVolume"] = beaker_2_reagents.total_volume
beaker2Data["maxVolume"] = beaker_2_reagents.maximum_volume
var/list/beakerContents = list()
for(var/datum/reagent/reagent as anything in beaker_2_reagents.reagent_list)
beakerContents += list(list(
"name" = reagent.name,
"volume" = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING),
"mass" = reagent.mass,
"purity" = round(reagent.purity * 100),
"type" = "Clean",
"log" = log[reagent.type]
))
beaker2Data["contents"] = beakerContents
.["beaker2"] = beaker2Data
/obj/machinery/chem_mass_spec/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
if(processing_reagents)
balloon_alert(ui.user, "still processing")
return ..()
switch(action)
if("activate")
if(!beaker1 || !beaker2 || !is_operational)
say("This [src] is missing an output beaker!")
if(QDELETED(beaker1))
say("Missing input beaker!")
return
if(processing_reagents)
say("You shouldn't be seeing this message! Please report this bug to https://github.com/tgstation/tgstation/issues . Thank you!")
stack_trace("Someone managed to break the HPLC and tried to get it to activate when it's already activated!")
if(QDELETED(beaker2))
say("Missing output beaker!")
return
processing_reagents = TRUE
estimate_time()
//adjust timer for purification
progress_time = 0
update_appearance()
estimate_time()
if(delay_time <= 0)
say("No work to be done!")
return
//start the purification process
processing_reagents = TRUE
begin_processing()
. = TRUE
update_appearance()
return TRUE
if("leftSlider")
if(!is_operational || processing_reagents)
var/value = params["value"]
if(isnull(value))
return
var/current_center = (lower_mass_range + upper_mass_range)/2
lower_mass_range = clamp(params["value"], calculate_smallest_mass(), current_center)
. = TRUE
value = text2num(value)
if(isnull(value))
return
lower_mass_range = clamp(value, calculate_mass(smallest = TRUE), (lower_mass_range + upper_mass_range) / 2)
estimate_time()
return TRUE
if("rightSlider")
if(!is_operational || processing_reagents)
var/value = params["value"]
if(isnull(value))
return
var/current_center = (lower_mass_range + upper_mass_range)/2
upper_mass_range = clamp(params["value"], current_center, calculate_largest_mass())
. = TRUE
value = text2num(value)
if(isnull(value))
return
upper_mass_range = clamp(value, (lower_mass_range + upper_mass_range) / 2, calculate_mass(smallest = FALSE))
estimate_time()
return TRUE
if("centerSlider")
if(!is_operational || processing_reagents)
var/value = params["value"]
if(isnull(value))
return
var/current_center = (lower_mass_range + upper_mass_range)/2
var/delta_center = current_center - params["value"]
var/lowest = calculate_smallest_mass()
var/highest = calculate_largest_mass()
value = text2num(value)
if(isnull(value))
return
var/delta_center = ((lower_mass_range + upper_mass_range) / 2) - params["value"]
var/lowest = calculate_mass(smallest = TRUE)
var/highest = calculate_mass(smallest = FALSE)
lower_mass_range = clamp(lower_mass_range - delta_center, lowest, highest)
upper_mass_range = clamp(upper_mass_range - delta_center, lowest, highest)
. = TRUE
estimate_time()
return TRUE
if("eject1")
if(processing_reagents)
return
replace_beaker(usr, BEAKER1)
. = TRUE
replace_beaker(ui.user, TRUE)
return TRUE
if("eject2")
if(processing_reagents)
return
replace_beaker(usr, BEAKER2)
. = TRUE
replace_beaker(ui.user, FALSE)
return TRUE
/* processing procs */
///Increments time if it's progressing - if it's past time then it purifies and stops processing
/obj/machinery/chem_mass_spec/process(seconds_per_tick)
/obj/machinery/chem_mass_spec/AltClick(mob/living/user)
. = ..()
if(!is_operational)
return FALSE
if(!can_interact(user))
return
if(processing_reagents)
balloon_alert(user, "still processing!")
return ..()
replace_beaker(user, TRUE)
/obj/machinery/chem_mass_spec/alt_click_secondary(mob/living/user)
. = ..()
if(!can_interact(user))
return
if(processing_reagents)
balloon_alert(user, "still processing!")
return ..()
replace_beaker(user, FALSE)
/obj/machinery/chem_mass_spec/process(seconds_per_tick)
if(!processing_reagents)
return TRUE
return PROCESS_KILL
if(!is_operational || panel_open || !anchored || (machine_stat & (BROKEN | NOPOWER)))
return
use_power(active_power_usage)
progress_time += seconds_per_tick
if(progress_time >= delay_time)
processing_reagents = FALSE
progress_time = 0
purify_reagents()
end_processing()
log.Cut()
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
//we don't bother about impure chems
if(istype(reagent, /datum/reagent/inverse) || (reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem))
continue
//out of our selected range
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
continue
//already at max purity
var/delta_purity = initial(reagent.purity) - reagent.purity
if(delta_purity <= 0)
continue
//add the purified reagent. More impure reagents will yield smaller amounts
var/product_vol = reagent.volume
beaker1.reagents.remove_reagent(reagent.type, product_vol)
beaker2.reagents.add_reagent(reagent.type, product_vol * (1 - delta_purity), reagtemp = beaker1.reagents.chem_temp, added_purity = initial(reagent.purity), added_ph = reagent.ph)
log[reagent.type] = "Purified to [initial(reagent.purity) * 100]%"
//recompute everything
lower_mass_range = calculate_mass(smallest = TRUE)
upper_mass_range = calculate_mass(smallest = FALSE)
estimate_time()
update_appearance()
return TRUE
progress_time += seconds_per_tick
return FALSE
/*
* Processing through the reagents in beaker 1
* For all the reagents within the selected range - we will then purify them up to their initial purity (usually 75%). It will take away the relative reagent volume from the sum volume of the reagent however.
* If there are any inverted reagents - then it will instead just create a new reagent of the inverted type. This doesn't really do anything other than change the name of it,
* As it processes through the reagents, it saves what changes were applied to each reagent in a log var to show the results at the end
*/
/obj/machinery/chem_mass_spec/proc/purify_reagents()
log = list()
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
//Inverse first
var/volume = reagent.volume
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
continue
log += list(inverse_reagent.type = "Cannot purify inverted") //Might as well make it do something - just updates the reagent's name
beaker2.reagents.add_reagent(reagent.inverse_chem, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = reagent.get_inverse_purity())
beaker1.reagents.remove_reagent(reagent.type, volume)
continue
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
continue
var/delta_purity = initial(reagent.purity) - reagent.purity
if(delta_purity <= 0)//As pure as we can be - so lets not add more than we need
log += list(reagent.type = "Can't purify over [initial(reagent.purity)*100]%")
beaker2.reagents.add_reagent(reagent.type, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = reagent.purity, added_ph = reagent.ph)
beaker1.reagents.remove_reagent(reagent.type, volume)
continue
var/product_vol = reagent.volume * (1-delta_purity)
beaker2.reagents.add_reagent(reagent.type, product_vol, reagtemp = beaker1.reagents.chem_temp, added_purity = initial(reagent.purity), added_ph = reagent.ph)
beaker1.reagents.remove_reagent(reagent.type, reagent.volume)
log += list(reagent.type = "Purified to [initial(reagent.purity)*100]%")
/* Mass spec graph calcs */
///Returns the largest mass to the nearest 50 (rounded up)
/obj/machinery/chem_mass_spec/proc/calculate_largest_mass()
if(!beaker1?.reagents)
return 0
var/max_mass = 0
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
max_mass = max(max_mass, inverse_reagent.mass)
continue
max_mass = max(max_mass, reagent.mass)
return CEILING(max_mass, 50)
///Returns the smallest mass to the nearest 50 (rounded down)
/obj/machinery/chem_mass_spec/proc/calculate_smallest_mass()
if(!beaker1?.reagents)
return 0
var/min_mass = 0
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
min_mass = min(min_mass, inverse_reagent.mass)
continue
min_mass = min(min_mass, reagent.mass)
return FLOOR(min_mass, 50)
/*
* Estimates how long the highlighted range will take to process
* The time will increase based off the reagent's volume, mass and purity.
* In most cases this is between 10 to 30s for a single reagent.
* This is why having a higher mass for a reagent is a balancing tool.
*/
/obj/machinery/chem_mass_spec/proc/estimate_time()
if(!beaker1?.reagents)
return 0
var/time = 0
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
continue
time += (((inverse_reagent.mass * reagent.volume) + (inverse_reagent.mass * reagent.purity * 0.1)) * 0.003) + 10 ///Roughly 10 - 30s?
continue
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
continue
time += (((reagent.mass * reagent.volume) + (reagent.mass * reagent.get_inverse_purity() * 0.1)) * 0.0035) + 10 ///Roughly 10 - 30s?
delay_time = (time * cms_coefficient)
return delay_time
#undef BEAKER1
#undef BEAKER2
return PROCESS_KILL
@@ -105,7 +105,7 @@
/obj/item/storage/portable_chem_mixer/ex_act(severity, target)
return severity > EXPLODE_LIGHT ? ..() : FALSE
/obj/item/storage/portable_chem_mixer/attackby(obj/item/weapon, mob/user, params)
/obj/item/storage/portable_chem_mixer/item_interaction(mob/living/user, obj/item/weapon, list/modifiers, is_right_clicking)
if (!atom_storage.locked || \
(weapon.item_flags & ABSTRACT) || \
(weapon.flags_1 & HOLOGRAM_1) || \
@@ -116,7 +116,7 @@
replace_beaker(user, weapon)
update_appearance()
return TRUE
return ITEM_INTERACT_SUCCESS
/**
* Replaces the beaker of the portable chemical mixer with another beaker, or simply adds the new beaker if none is in currently
@@ -185,11 +185,11 @@
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
.["beaker"] = beaker_data
-389
View File
@@ -1,389 +0,0 @@
import { round } from 'common/math';
import { useBackend } from '../backend';
import {
Box,
Button,
Dimmer,
Icon,
Section,
Slider,
Table,
} from '../components';
import { Window } from '../layouts';
export const MassSpec = (props) => {
const { act, data } = useBackend();
const {
processing,
lowerRange,
upperRange,
graphUpperRange,
graphLowerRange,
eta,
beaker1CurrentVolume,
beaker2CurrentVolume,
beaker1MaxVolume,
beaker2MaxVolume,
peakHeight,
beaker1,
beaker2,
beaker1Contents = [],
beaker2Contents = [],
} = data;
const centerValue = (lowerRange + upperRange) / 2;
return (
<Window width={490} height={650}>
<Window.Content scrollable>
{!!processing && (
<Dimmer fontSize="32px">
<Icon name="cog" spin={1} />
{' Purifying... ' + round(eta) + 's'}
</Dimmer>
)}
<Section
title="Mass Spectroscopy"
buttons={
<Button
icon="power-off"
content="Start"
disabled={!!processing || !beaker1Contents.length || !beaker2}
tooltip={
!beaker1Contents.length
? 'Missing input reagents!'
: !beaker2
? 'Missing an output beaker!'
: 'Begin purifying'
}
tooltipPosition="left"
onClick={() => act('activate')}
/>
}
>
{(beaker1Contents.length && (
<MassSpectroscopy
lowerRange={lowerRange}
centerValue={centerValue}
upperRange={upperRange}
graphLowerRange={graphLowerRange}
graphUpperRange={graphUpperRange}
maxAbsorbance={peakHeight}
reagentPeaks={beaker1Contents}
/>
)) || <Box>Please insert an input beaker with reagents!</Box>}
</Section>
<Section
title="Input beaker"
buttons={
!!beaker1Contents && (
<>
{!!beaker1MaxVolume && (
<Box inline color="label" mr={2}>
{beaker1CurrentVolume} / {beaker1MaxVolume} units
</Box>
)}
<Button
icon="eject"
content="Eject"
disabled={!beaker1}
onClick={() => act('eject1')}
/>
</>
)
}
>
<BeakerMassProfile loaded={!!beaker1} beaker={beaker1Contents} />
{!!beaker1Contents.length && (
<Box>{'Eta of selection: ' + round(eta) + ' seconds'}</Box>
)}
</Section>
<Section
title="Output beaker"
buttons={
!!beaker2Contents && (
<>
{!!beaker2MaxVolume && (
<Box inline color="label" mr={2}>
{beaker2CurrentVolume} / {beaker2MaxVolume} units
</Box>
)}
<Button
icon="eject"
content="Eject"
disabled={!beaker2}
onClick={() => act('eject2')}
/>
</>
)
}
>
<BeakerMassProfile
loaded={!!beaker2}
beaker={beaker2Contents}
details
/>
</Section>
</Window.Content>
</Window>
);
};
const BeakerMassProfile = (props) => {
const { loaded, details, beaker = [] } = props;
return (
<Box>
{(!loaded && <Box color="label">No beaker loaded.</Box>) ||
(beaker.length === 0 && <Box color="label">Beaker is empty.</Box>) || (
<Table className="candystripe">
<Table.Row>
<Table.Cell bold collapsing color="label">
Reagent
</Table.Cell>
<Table.Cell bold collapsing color="label">
Volume
</Table.Cell>
<Table.Cell bold collapsing color="label">
Mass
</Table.Cell>
<Table.Cell bold collapsing color="label">
Purity
</Table.Cell>
<Table.Cell bold collapsing color="label">
Type
</Table.Cell>
{!!details && (
<Table.Cell bold collapsing color="label">
Results
</Table.Cell>
)}
</Table.Row>
{beaker.map((reagent) => (
<Table.Row key={reagent.name}>
<Table.Cell
collapsing
color={reagent.selected ? 'green' : 'default'}
>
{reagent.name}
</Table.Cell>
<Table.Cell
collapsing
color={reagent.selected ? 'green' : 'default'}
>
{reagent.volume}
</Table.Cell>
<Table.Cell
collapsing
color={reagent.selected ? 'green' : 'default'}
>
{reagent.mass}
</Table.Cell>
<Table.Cell
collapsing
color={reagent.selected ? 'green' : 'default'}
>
{`${reagent.purity}%`}
</Table.Cell>
<Table.Cell collapsing color={reagent.color}>
{reagent.type}
</Table.Cell>
{!!details && <Table.Cell>{reagent.log}</Table.Cell>}
</Table.Row>
))}
</Table>
)}
</Box>
);
};
const MassSpectroscopy = (props) => {
const { act, data } = useBackend();
const {
lowerRange,
centerValue,
upperRange,
graphUpperRange,
graphLowerRange,
maxAbsorbance,
reagentPeaks = [],
} = props;
const deltaRange = graphUpperRange - graphLowerRange;
const graphIncrement = deltaRange * 0.2;
return (
<>
<Box position="absolute" x="200" transform="translate(30,30)">
<svg background-size="200px" width="200" height="400">
<text
x="0"
y="250"
text-anchor="middle"
fill="white"
font-size="16"
transform="translate(0,0) scale(0.8 0.8)"
>
{/* x axis*/}
<tspan x="250" y="318" font-weight="bold" font-size="1.4em">
Mass (g)
</tspan>
<tspan x="0" y="283">
{graphLowerRange}
</tspan>
<tspan x="100" y="283">
{round(graphLowerRange + graphIncrement, 1)}
</tspan>
<tspan x="200" y="283">
{round(graphLowerRange + graphIncrement * 2, 1)}
</tspan>
<tspan x="300" y="283">
{round(graphLowerRange + graphIncrement * 3, 1)}
</tspan>
<tspan x="400" y="283">
{round(graphLowerRange + graphIncrement * 4, 1)}
</tspan>
<tspan x="500" y="283">
{graphUpperRange}
</tspan>
{/* y axis*/}
<tspan x="520" y="0" dy="6">
{round(maxAbsorbance, 1)}
</tspan>
<tspan x="520" y="50" dy="6">
{round(maxAbsorbance * 0.8, 1)}
</tspan>
<tspan x="520" y="100" dy="6">
{round(maxAbsorbance * 0.6, 1)}
</tspan>
<tspan x="520" y="150" dy="6">
{round(maxAbsorbance * 0.4, 1)}
</tspan>
<tspan x="520" y="200" dy="6">
{round(maxAbsorbance * 0.2, 1)}
</tspan>
<tspan x="520" y="250" dy="6">
0
</tspan>
</text>
<text
text-anchor="middle"
transform="translate(430,100) rotate(90) scale(0.8 0.8)"
fill="white"
font-size="16"
>
<tspan font-weight="bold" font-size="1.4em">
Absorbance (AU)
</tspan>
</text>
<g transform="translate(0, 0) scale(0.8 0.8)">
{reagentPeaks.map((peak) => (
// Triangle peak
<polygon
key={peak.name}
points={`${((peak.mass - 10) / graphUpperRange) * 500},265 ${
(peak.mass / graphUpperRange) * 500
},${250 - (peak.volume / maxAbsorbance) * 250} ${
((peak.mass + 10) / graphUpperRange) * 500
},265 `}
opacity="0.6"
style={{ fill: peak.color }}
/>
))}
<polygon
points={`${(lowerRange / deltaRange) * 500},265 ${
(lowerRange / deltaRange) * 500
},0 ${(upperRange / deltaRange) * 500},0 ${
(upperRange / deltaRange) * 500
},265`}
opacity="0.2"
style={{ fill: 'blue' }}
/>
<line
x1={0}
y1={265}
x2={502}
y2={264}
stroke={'white'}
stroke-width={3}
/>
<line
x1={501}
y1={264}
x2={501}
y2={0}
stroke={'white'}
stroke-width={3}
/>
</g>
</svg>
</Box>
<Box>
<Slider
name={'Left slider'}
position="relative"
step={graphUpperRange / 400}
height={17.2}
format={(value) => round(value)}
width={(centerValue / graphUpperRange) * 400 + 'px'}
value={lowerRange}
minValue={graphLowerRange}
maxValue={centerValue}
color={'invisible'}
onDrag={(e, value) =>
act('leftSlider', {
value: value,
})
}
>
{' '}
</Slider>
<Slider
name={'Right slider'}
position="absolute"
height={17.2}
format={(value) => round(value)}
step={graphUpperRange / 400}
width={400 - (centerValue / graphUpperRange) * 400 + 'px'}
value={upperRange}
minValue={centerValue}
maxValue={graphUpperRange}
color={'invisible'}
onDrag={(e, value) =>
act('rightSlider', {
value: value,
})
}
>
{' '}
</Slider>
<Box>
<Slider
name={'Center slider'}
position="relative"
step={graphUpperRange / 400}
mt={0.3}
mb={5}
value={centerValue}
height={1.9}
format={(value) => round(value)}
width={400 + 'px'}
minValue={graphLowerRange + 1}
maxValue={graphUpperRange - 1}
color={'invisible'}
onDrag={(e, value) =>
act('centerSlider', {
value: value,
})
}
>
{' '}
</Slider>
</Box>
</Box>
</>
);
};
+456
View File
@@ -0,0 +1,456 @@
import { round } from 'common/math';
import { BooleanLike } from 'common/react';
import { useBackend } from '../backend';
import {
Box,
Button,
Dimmer,
Icon,
Section,
Slider,
Table,
} from '../components';
import { Window } from '../layouts';
type Reagent = {
name: string;
volume: number;
mass: number;
purity: number;
type: string;
log: string;
};
type Beaker = {
currentVolume: number;
maxVolume: number;
contents: Reagent[];
};
type Data = {
lowerRange: number;
upperRange: number;
processing: BooleanLike;
eta: number;
graphUpperRange: number;
peakHeight: number;
beaker1: Beaker;
beaker2: Beaker;
};
const GRAPH_MAX_WIDTH = 1060;
const GRAPH_MAX_HEIGHT = 250;
export const MassSpec = (props) => {
const { act, data } = useBackend<Data>();
const {
processing,
lowerRange,
upperRange,
graphUpperRange,
eta,
peakHeight,
beaker1,
beaker2,
} = data;
const centerValue = (lowerRange + upperRange) / 2;
const beaker_1_has_contents = beaker1?.contents?.length > 0;
return (
<Window width={1050} height={650}>
<Window.Content scrollable>
{!!processing && (
<Dimmer fontSize="32px">
<Icon name="cog" spin={1} />
{' Purifying... ' + round(eta, 0) + 's'}
</Dimmer>
)}
<Section
title="Mass Spectroscopy"
buttons={
<Button
icon="power-off"
disabled={
!!processing || eta <= 0 || !beaker_1_has_contents || !beaker2
}
tooltip={
!beaker_1_has_contents
? 'Missing input reagents!'
: !beaker2
? 'Missing an output beaker!'
: eta <= 0
? 'No work to be done'
: 'Begin purifying'
}
tooltipPosition="left"
onClick={() => act('activate')}
>
Start
</Button>
}
>
{(beaker_1_has_contents && (
<MassSpectroscopy
lowerRange={lowerRange}
centerValue={centerValue}
upperRange={upperRange}
graphUpperRange={graphUpperRange}
maxAbsorbance={peakHeight}
reagentPeaks={beaker1.contents}
/>
)) || <Box>Please insert an input beaker with reagents!</Box>}
</Section>
<Section
title="Input beaker"
buttons={
!!beaker1 && (
<>
{
<Box inline color="label" mr={2}>
{beaker1.currentVolume} / {beaker1.maxVolume} units
</Box>
}
<Button icon="eject" onClick={() => act('eject1')}>
Eject
</Button>
</>
)
}
>
<BeakerMassProfile
lowerRange={lowerRange}
upperRange={upperRange}
beaker={beaker1}
/>
{!!beaker_1_has_contents && (
<Box>{'Eta of selection: ' + round(eta, 0) + ' seconds'}</Box>
)}
</Section>
<Section
title="Output beaker"
buttons={
!!beaker2 && (
<>
{
<Box inline color="label" mr={2}>
{beaker2.currentVolume} / {beaker2.maxVolume} units
</Box>
}
<Button icon="eject" onClick={() => act('eject2')}>
Eject
</Button>
</>
)
}
>
<BeakerMassProfile
lowerRange={lowerRange}
upperRange={upperRange}
beaker={beaker2}
/>
</Section>
</Window.Content>
</Window>
);
};
type ProfileProps = {
lowerRange: number;
upperRange: number;
beaker: Beaker;
};
const BeakerMassProfile = (props: ProfileProps) => {
const { lowerRange, upperRange, beaker } = props;
return (
<Box>
{(!beaker && <Box color="label">No beaker loaded.</Box>) ||
(beaker.contents.length === 0 && (
<Box color="label">Beaker is empty.</Box>
)) || (
<Table className="candystripe">
<Table.Row>
<Table.Cell bold collapsing color="label">
Reagent
</Table.Cell>
<Table.Cell bold collapsing color="label">
Mass
</Table.Cell>
<Table.Cell bold collapsing color="label">
Volume
</Table.Cell>
<Table.Cell bold collapsing color="label">
Purity
</Table.Cell>
<Table.Cell bold collapsing color="label">
Type
</Table.Cell>
<Table.Cell bold collapsing color="label">
Status
</Table.Cell>
</Table.Row>
{beaker.contents.map((reagent) => {
const selected =
reagent.mass >= lowerRange && reagent.mass <= upperRange;
const color = reagent.type === 'Inverted' ? '#b60046' : '#3cf096';
return (
<Table.Row key={reagent.name}>
<Table.Cell collapsing color={selected ? 'green' : 'default'}>
{reagent.name}
</Table.Cell>
<Table.Cell collapsing color={selected ? 'green' : 'default'}>
{reagent.mass}
</Table.Cell>
<Table.Cell collapsing color={selected ? 'green' : 'default'}>
{reagent.volume}
</Table.Cell>
<Table.Cell collapsing color={selected ? 'green' : 'default'}>
{`${reagent.purity}%`}
</Table.Cell>
<Table.Cell collapsing color={color}>
{reagent.type}
</Table.Cell>
{<Table.Cell>{reagent.log}</Table.Cell>}
</Table.Row>
);
})}
</Table>
)}
</Box>
);
};
type SpectroscopyProps = {
lowerRange: number;
centerValue: number;
upperRange: number;
graphUpperRange: number;
maxAbsorbance: number;
reagentPeaks: Reagent[];
};
const MassSpectroscopy = (props: SpectroscopyProps) => {
const { act } = useBackend();
const {
lowerRange,
centerValue,
upperRange,
graphUpperRange,
maxAbsorbance,
reagentPeaks = [],
} = props;
const graphLowerRange = 0;
const deltaRange = graphUpperRange - graphLowerRange;
const graphIncrement = deltaRange * 0.2;
const base_line = GRAPH_MAX_HEIGHT * 0.85;
const base_width = GRAPH_MAX_WIDTH - 123;
const x_scale = base_width / GRAPH_MAX_WIDTH;
const y_scale = base_line / GRAPH_MAX_HEIGHT;
return (
<Box
style={{
width: `${GRAPH_MAX_WIDTH}px`,
height: `${GRAPH_MAX_HEIGHT}px`,
}}
>
<svg
style={{
position: 'absolute',
width: `${GRAPH_MAX_WIDTH}px`,
height: `${GRAPH_MAX_HEIGHT}px`,
top: '10px',
}}
>
{/* x axis*/}
<text
text-anchor="middle"
fill="white"
transform={`scale(${x_scale} 1)`}
font-size="14"
>
<tspan
x="40%"
y={`${base_line + 40}px`}
font-weight="bold"
font-size="16"
>
Mass (G)
</tspan>
<tspan x="0%" y={`${base_line + 20}px`}>
{graphLowerRange}
</tspan>
<tspan x="20%" y={`${base_line + 20}px`}>
{round(graphLowerRange + graphIncrement, 1)}
</tspan>
<tspan x="40%" y={`${base_line + 20}px`}>
{round(graphLowerRange + graphIncrement * 2, 1)}
</tspan>
<tspan x="60%" y={`${base_line + 20}px`}>
{round(graphLowerRange + graphIncrement * 3, 1)}
</tspan>
<tspan x="80%" y={`${base_line + 20}px`}>
{round(graphLowerRange + graphIncrement * 4, 1)}
</tspan>
<tspan x="100%" y={`${base_line + 20}px`}>
{graphUpperRange}
</tspan>
</text>
<line
x1={0}
y1={base_line}
x2={base_width}
y2={base_line}
stroke={'white'}
stroke-width={3}
/>
{/* y axis*/}
<text
text-anchor="middle"
fill="white"
transform={`scale(1 ${y_scale})`}
font-size="14"
>
<tspan x={`${base_width + 20}px`} y="100%">
0
</tspan>
<tspan x={`${base_width + 20}px`} y="80%">
{round(maxAbsorbance * 0.2, 1)}
</tspan>
<tspan x={`${base_width + 20}px`} y="60%">
{round(maxAbsorbance * 0.4, 1)}
</tspan>
<tspan x={`${base_width + 20}px`} y="40%">
{round(maxAbsorbance * 0.6, 1)}
</tspan>
<tspan x={`${base_width + 20}px`} y="20%">
{round(maxAbsorbance * 0.8, 1)}
</tspan>
<tspan x={`${base_width + 20}px`} y="0%">
{round(maxAbsorbance, 1)}
</tspan>
</text>
<text
text-anchor="middle"
transform={`translate(${base_width + 35},${
GRAPH_MAX_HEIGHT * 0.4
}) rotate(90) scale(1, 1.2)`}
fill="white"
font-size="17"
font-weight="bold"
>
<tspan>Absorbance (AU)</tspan>
</text>
<line
x1={base_width}
y1={base_line}
x2={base_width}
y2={0}
stroke={'white'}
stroke-width={3}
/>
{/* Graph */}
<g transform={`scale(${x_scale} ${y_scale})`}>
{reagentPeaks.map((peak) => (
<>
{/* Triangle peak */}
<polygon
key={peak.name}
points={`${
((peak.mass - 5) / graphUpperRange) * GRAPH_MAX_WIDTH
},${GRAPH_MAX_HEIGHT}
${(peak.mass / graphUpperRange) * GRAPH_MAX_WIDTH},${
GRAPH_MAX_HEIGHT -
(peak.volume / maxAbsorbance) * GRAPH_MAX_HEIGHT
}
${
((peak.mass + 5) / graphUpperRange) * GRAPH_MAX_WIDTH
}, ${GRAPH_MAX_HEIGHT}`}
opacity="0.6"
style={{
fill: peak.type === 'Inverted' ? '#b60046' : '#3cf096',
}}
/>
{/* Background */}
<polygon
points={`${
(lowerRange / deltaRange) * GRAPH_MAX_WIDTH
},${GRAPH_MAX_HEIGHT} ${
(lowerRange / deltaRange) * GRAPH_MAX_WIDTH
},0 ${(upperRange / deltaRange) * GRAPH_MAX_WIDTH},0 ${
(upperRange / deltaRange) * GRAPH_MAX_WIDTH
},${GRAPH_MAX_HEIGHT}`}
opacity="0.1"
style={{ fill: 'blue' }}
/>
</>
))}
</g>
</svg>
{/* Sliders */}
<Slider
name={'Left slider'}
step={graphUpperRange / base_width}
suppressFlicker
height={17.2}
format={(value: number) => round(value, 2)}
width={(centerValue / graphUpperRange) * base_width + 'px'}
value={lowerRange}
minValue={graphLowerRange}
maxValue={centerValue}
color={'invisible'}
onDrag={(e, value) =>
act('leftSlider', {
value: value,
})
}
/>
<Slider
name={'Right slider'}
height={17.2}
suppressFlicker
format={(value: number) => round(value, 2)}
step={graphUpperRange / base_width}
width={base_width - (centerValue / graphUpperRange) * base_width + 'px'}
value={upperRange}
minValue={centerValue}
maxValue={graphUpperRange}
color={'invisible'}
onDrag={(e, value) =>
act('rightSlider', {
value: value,
})
}
/>
<Slider
name={'Center slider'}
step={graphUpperRange / base_width}
suppressFlicker
mt={1.2}
value={centerValue}
height={1.9}
format={(value: number) => round(value, 2)}
width={base_width + 'px'}
minValue={graphLowerRange + 1}
maxValue={graphUpperRange - 1}
color={'invisible'}
onDrag={(e, value) =>
act('centerSlider', {
value: value,
})
}
/>
</Box>
);
};