Fermichem 2.6 - Adds the HPLC: a new roundstart method of detecting purity, as well as a method of partially purifying reagents (#57858)

Adds the High-performance liquid chromatography machine - a new roundstart method of detecting purity, as well as a method of partially purifying reagents.

A single machine has been added to each of the 4(5) maps - this lets people determine their purity based off the size of the green (pure portion) to the orange (impure portion) of a reagent's peak shown on the mas spectroscopy display. If a reagent is impure - it will be displayed as red.

In addition to this function, the HPLC can purify reagents by selecting a range of them and pressing the purify button. This will cost some time depending on the mass of the reagent and will purify it up it's default purity (in the tweaked reagent cases - 75%, for the reagents I've yet to get to - 100%). It will also reduce the volume accordingly (so in essence you are reducing the volume of the impure parts).

The sprite itself will indicate when it's running, so you don't need to be nearby it or use the UI to know when it's done
This commit is contained in:
Thalpy
2021-03-26 09:56:53 +00:00
committed by GitHub
parent af0d7f553f
commit eee51bcaaa
16 changed files with 721 additions and 32 deletions

View File

@@ -16,6 +16,7 @@
if(path in GLOB.fake_reagent_blacklist)
continue
var/datum/reagent/D = new path()
D.mass = rand(10, 800) //This is terrible and should be removed ASAP!
GLOB.chemical_reagents_list[path] = D
/proc/build_chemical_reactions_lists()

View File

@@ -0,0 +1,366 @@
#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 corrisponding beaker too."}
density = TRUE
layer = BELOW_OBJ_LAYER
icon = 'icons/obj/chemical.dmi'
icon_state = "HPLC"
base_icon_state = "HPLC"
use_power = IDLE_POWER_USE
idle_power_usage = 20
resistance_flags = FIRE_PROOF | ACID_PROOF
///If we're processing reagents or not
var/processing_reagents = FALSE
///Time we started processing + the delay
var/delay_time = 0
///How much time we've done so far
var/progress_time = 0
///Lower mass range - for mass selection of what will be processed
var/lower_mass_range = 0
///Upper_mass_range - for mass selection of what will be processed
var/upper_mass_range = INFINITY
///The log output to clarify how the thing works
var/list/log = list()
///Input reagents container
var/obj/item/reagent_containers/beaker1
///Output reagents container
var/obj/item/reagent_containers/beaker2
/obj/machinery/chem_mass_spec/Initialize()
. = ..()
beaker2 = new /obj/item/reagent_containers/glass/beaker/large(src)
ADD_TRAIT(src, DO_NOT_SPLASH, src.type)
/obj/machinery/chem_mass_spec/Destroy()
QDEL_NULL(beaker1)
QDEL_NULL(beaker2)
return ..()
/* beaker swapping/attack code */
///Adds beaker 1
/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(istype(item, /obj/item/reagent_containers) && !(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 class='notice'>You add [beaker] to [src].</span>")
updateUsrDialog()
update_appearance()
..()
///Adds beaker 2
/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(istype(item, /obj/item/reagent_containers) && !(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 class='notice'>You add [beaker] to [src].</span>")
updateUsrDialog()
update_appearance()
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/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.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
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.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
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)
. += "HPLC_beaker1"
if(beaker2)
. += "HPLC_beaker2"
if(powered())
if(processing_reagents)
. += "HPLC_graph_active"
else if (length(beaker1?.reagents.reagent_list))
. += "HPLC_graph_idle"
/* UI Code */
/obj/machinery/chem_mass_spec/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "MassSpec", name)
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()
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" = 1-reagent.purity, "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 * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "selected" = in_range, "color" = "#3cf096", "type" = "Clean")))
if(1 > reagent.purity && reagent.impure_chem)
var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "selected" = in_range, "color" = "#fc9738", "type" = "Impurity")))
data["peakHeight"] = max(data["peakHeight"], reagent.volume * (1-reagent.purity))
data["peakHeight"] = max(data["peakHeight"], reagent.volume * reagent.purity)
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
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 * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "color" = "#3cf096", "type" = "Clean", log = log[reagent.type])))
///Impure stuff
if(1 > reagent.purity && reagent.impure_chem)
var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "color" = "#fc9738", "type" = "Impurity")))
data["beaker2CurrentVolume"] = beaker2.reagents.total_volume
data["beaker2MaxVolume"] = beaker2.reagents.maximum_volume
data["beaker2Contents"] = beakerContents
return data
/obj/machinery/chem_mass_spec/ui_act(action, params)
. = ..()
if(.)
return
switch(action)
if("activate")
if(!beaker1 || !beaker2 || !is_operational)
say("This [src] is missing an output 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!")
return
processing_reagents = TRUE
estimate_time()
progress_time = 0
update_appearance()
begin_processing()
. = TRUE
if("leftSlider")
if(!is_operational || processing_reagents)
return
var/current_center = (lower_mass_range + upper_mass_range)/2
lower_mass_range = clamp(params["value"], calculate_smallest_mass(), current_center)
. = TRUE
if("rightSlider")
if(!is_operational || processing_reagents)
return
var/current_center = (lower_mass_range + upper_mass_range)/2
upper_mass_range = clamp(params["value"], current_center, calculate_largest_mass())
. = TRUE
if("centerSlider")
if(!is_operational || processing_reagents)
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()
lower_mass_range = clamp(lower_mass_range - delta_center, lowest, highest)
upper_mass_range = clamp(upper_mass_range - delta_center, lowest, highest)
. = TRUE
if("eject1")
if(processing_reagents)
return
replace_beaker(usr, BEAKER1)
. = TRUE
if("eject2")
if(processing_reagents)
return
replace_beaker(usr, BEAKER2)
. = 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(delta_time)
. = ..()
if(!is_operational)
return FALSE
if(!processing_reagents)
return TRUE
if(progress_time >= delay_time)
processing_reagents = FALSE
progress_time = 0
purify_reagents()
end_processing()
update_appearance()
return TRUE
progress_time += delta_time
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 = 1-reagent.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
var/inverse_purity = 1-reagent.purity
time += (((reagent.mass * reagent.volume) + (reagent.mass * inverse_purity * 0.1)) * 0.0035) + 10 ///Roughly 10 - 30s?
delay_time = time
return delay_time

View File

@@ -6,7 +6,7 @@
name = "chemical reaction tester"
density = TRUE
icon = 'icons/obj/chemical.dmi'
icon_state = "HPLC"
icon_state = "HPLC_debug"
use_power = IDLE_POWER_USE
idle_power_usage = 40
resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE

View File

@@ -52,6 +52,8 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
var/purity = 1
///the purity of the reagent on creation (i.e. when it's added to a mob and it's purity split it into 2 chems; the purity of the resultant chems are kept as 1, this tracks what the purity was before that)
var/creation_purity = 1
///The molar mass of the reagent - if you're adding a reagent that doesn't have a recipe, just add a random number between 10 - 800. Higher numbers are "harder" but it's mostly arbitary.
var/mass
/// color it looks in containers etc
var/color = "#000000" // rgb: 0, 0, 0
///how fast the reagent is metabolized by the mob
@@ -97,6 +99,7 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
///The amount a robot will pay for a glass of this (20 units but can be higher if you pour more, be frugal!)
var/glass_price
/datum/reagent/New()
SHOULD_CALL_PARENT(TRUE)
. = ..()
@@ -105,6 +108,8 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
material = GET_MATERIAL_REF(material)
if(glass_price)
AddElement(/datum/element/venue_price, glass_price)
if(!mass)
mass = rand(10, 800)
/datum/reagent/Destroy() // This should only be called by the holder, so it's already handled clearing its references
. = ..()

View File

@@ -60,6 +60,8 @@
return
/obj/item/reagent_containers/pre_attack_secondary(atom/target, mob/living/user, params)
if(HAS_TRAIT(target, DO_NOT_SPLASH))
return ..()
if (try_splash(user, target))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN