diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm index ad67850f3b2..e8f219acc56 100644 --- a/_maps/map_files/Deltastation/DeltaStation2.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2.dmm @@ -37558,6 +37558,7 @@ /obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{ dir = 8 }, +/obj/machinery/chem_mass_spec, /turf/open/floor/iron/white, /area/medical/pharmacy) "dkl" = ( diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index 41ed4122675..eb54daea5ea 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -12904,7 +12904,6 @@ /obj/machinery/door/firedoor, /obj/machinery/door/airlock/hatch{ name = "Morgue"; - req_access = null; req_access_txt = "6" }, /turf/open/floor/iron/dark, @@ -14482,7 +14481,6 @@ /obj/machinery/door/firedoor, /obj/machinery/door/airlock/hatch{ name = "Morgue"; - req_access = null; req_access_txt = "6;5" }, /obj/structure/disposalpipe/segment{ @@ -14695,8 +14693,6 @@ /obj/effect/turf_decal/tile/yellow{ dir = 1 }, -/obj/item/hand_labeler, -/obj/structure/table, /obj/machinery/firealarm{ pixel_y = 24 }, @@ -14704,6 +14700,8 @@ c_tag = "Pharmacy"; network = list("ss13","medbay") }, +/obj/structure/closet/secure_closet/chemical, +/obj/item/radio/headset/headset_med, /turf/open/floor/iron/white, /area/medical/pharmacy) "bnM" = ( @@ -38019,14 +38017,7 @@ /obj/effect/turf_decal/tile/yellow{ dir = 4 }, -/obj/item/book/manual/wiki/chemistry{ - pixel_x = 8 - }, -/obj/item/book/manual/wiki/grenades, -/obj/item/book/manual/wiki/plumbing{ - pixel_x = 5 - }, -/obj/structure/table, +/obj/machinery/chem_mass_spec, /turf/open/floor/iron/white, /area/medical/pharmacy) "kGQ" = ( @@ -42748,8 +42739,14 @@ /obj/effect/turf_decal/tile/yellow{ dir = 1 }, -/obj/structure/closet/secure_closet/chemical, -/obj/item/radio/headset/headset_med, +/obj/structure/table, +/obj/item/book/manual/wiki/grenades, +/obj/item/book/manual/wiki/plumbing{ + pixel_x = 5 + }, +/obj/item/book/manual/wiki/chemistry{ + pixel_x = 8 + }, /turf/open/floor/iron/white, /area/medical/pharmacy) "nRi" = ( @@ -44788,7 +44785,6 @@ }, /obj/machinery/button/door{ id = "AuxToilet3"; - id_tag = null; name = "Lock Control"; normaldoorcontrol = 1; pixel_y = -25; @@ -45888,7 +45884,6 @@ }, /obj/machinery/door/airlock/command{ name = "Chief Medical Officer"; - req_access = null; req_access_txt = "40" }, /obj/structure/cable, @@ -55332,6 +55327,8 @@ }, /obj/machinery/power/apc/auto_name/north, /obj/structure/cable, +/obj/structure/table, +/obj/item/hand_labeler, /turf/open/floor/iron/white, /area/medical/pharmacy) "wEo" = ( @@ -57563,7 +57560,6 @@ }, /obj/machinery/button/door{ id = "AuxToilet4"; - id_tag = null; name = "Lock Control"; normaldoorcontrol = 1; pixel_y = -25; diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm index 74c638449c4..5d577fa232d 100644 --- a/_maps/map_files/KiloStation/KiloStation.dmm +++ b/_maps/map_files/KiloStation/KiloStation.dmm @@ -18793,6 +18793,9 @@ /obj/effect/turf_decal/tile/yellow{ dir = 1 }, +/obj/item/kirbyplants{ + icon_state = "plant-10" + }, /turf/open/floor/iron/showroomfloor, /area/medical/pharmacy) "aTG" = ( @@ -19013,12 +19016,10 @@ /obj/machinery/firealarm{ pixel_y = 26 }, -/obj/item/kirbyplants{ - icon_state = "plant-10" - }, /obj/structure/disposalpipe/segment{ dir = 4 }, +/obj/machinery/chem_mass_spec, /turf/open/floor/iron/showroomfloor, /area/medical/pharmacy) "aTZ" = ( @@ -66367,7 +66368,6 @@ id = "IsolationFlash"; pixel_x = -23; pixel_y = 8; - req_access = null; req_access_txt = "2" }, /obj/machinery/button/door{ diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index 6f1c5489022..791333816d9 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -6974,7 +6974,6 @@ name = "Brig Lockdown Control"; pixel_x = 6; pixel_y = 7; - req_access = null; req_access_txt = "63" }, /obj/machinery/button/door{ @@ -22657,10 +22656,10 @@ /area/medical/chemistry) "cmC" = ( /obj/effect/turf_decal/tile/yellow, -/obj/structure/closet/secure_closet/chemical, /obj/effect/turf_decal/tile/yellow{ dir = 4 }, +/obj/machinery/chem_mass_spec, /turf/open/floor/iron/white, /area/medical/pharmacy) "cmD" = ( @@ -35379,10 +35378,7 @@ /turf/open/floor/iron, /area/maintenance/starboard) "fuY" = ( -/obj/machinery/holopad, -/obj/effect/turf_decal/box/white{ - color = "#EFB341" - }, +/obj/structure/closet/secure_closet/chemical, /turf/open/floor/iron/white, /area/medical/pharmacy) "fvb" = ( @@ -47093,6 +47089,10 @@ /turf/open/floor/wood, /area/maintenance/port/aft) "kge" = ( +/obj/machinery/holopad, +/obj/effect/turf_decal/box/white{ + color = "#EFB341" + }, /turf/open/floor/iron/white, /area/medical/pharmacy) "kgg" = ( @@ -55272,7 +55272,7 @@ }, /obj/item/storage/fancy/donut_box, /obj/item/paper{ - info = "Jim Norton's Quebecois Coffee. You see, in 2265 the Quebecois had finally had enough of Canada's shit, and went to the one place that wasn't corrupted by Canuckistan.Je vais au seul endroit qui n'a pas � corrompu par les Canadiens ... ESPACE."; + info = "Jim Norton's Quebecois Coffee. You see, in 2265 the Quebecois had finally had enough of Canada's shit, and went to the one place that wasn't corrupted by Canuckistan.Je vais au seul endroit qui n'a pas ??? corrompu par les Canadiens ... ESPACE."; name = "Coffee Shop"; pixel_x = -4; pixel_y = 6 diff --git a/_maps/map_files/debug/runtimestation.dmm b/_maps/map_files/debug/runtimestation.dmm index 29984d17944..47de3992598 100644 --- a/_maps/map_files/debug/runtimestation.dmm +++ b/_maps/map_files/debug/runtimestation.dmm @@ -2214,6 +2214,11 @@ }, /turf/open/floor/iron, /area/hallway/primary/central) +"mE" = ( +/obj/structure/cable, +/obj/machinery/chem_mass_spec, +/turf/open/floor/iron, +/area/medical/chemistry) "nn" = ( /obj/machinery/atmospherics/components/unary/vent_pump/on{ dir = 8 @@ -7637,7 +7642,7 @@ aj bP Ce JF -cj +mE oV bE bE diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 5969c996397..a6f4b6ec8ae 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -310,6 +310,9 @@ ///from base of atom/AltClick(): (/mob) #define COMSIG_CLICK_ALT "alt_click" #define COMPONENT_CANCEL_CLICK_ALT (1<<0) +///from base of atom/alt_click_secondary(): (/mob) +#define COMSIG_CLICK_ALT_SECONDARY "alt_click_secondary" + #define COMPONENT_CANCEL_CLICK_ALT_SECONDARY (1<<0) ///from base of atom/CtrlShiftClick(/mob) #define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click" ///from base of atom/MouseDrop(): (/atom/over, /mob/user) @@ -444,7 +447,8 @@ ///from base of mob/AltClickOn(): (atom/A) #define COMSIG_MOB_ALTCLICKON "mob_altclickon" #define COMSIG_MOB_CANCEL_CLICKON (1<<0) - +///from base of mob/alt_click_on_secodary(): (atom/A) +#define COMSIG_MOB_ALTCLICKON_SECONDARY "mob_altclickon_secondary" /// From base of /mob/living/simple_animal/bot/proc/bot_step() #define COMSIG_MOB_BOT_PRE_STEP "mob_bot_pre_step" /// Should always match COMPONENT_MOVABLE_BLOCK_PRE_MOVE as these are interchangeable and used to block movement. diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 1f2acefb0b6..5470cfa9c0d 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -453,6 +453,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Trait associated with highlander #define HIGHLANDER_TRAIT "highlander" +///generic atom traits +#define DO_NOT_SPLASH "do_not_splash" + // unique trait sources, still defines #define CLONING_POD_TRAIT "cloning-pod" #define STATUE_MUTE "statue" diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 76a3aa0110b..ccab871f4a8 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -92,7 +92,10 @@ MiddleClickOn(A, params) return if(LAZYACCESS(modifiers, ALT_CLICK)) // alt and alt-gr (rightalt) - AltClickOn(A) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + alt_click_on_secondary(A) + else + AltClickOn(A) return if(LAZYACCESS(modifiers, CTRL_CLICK)) CtrlClickOn(A) @@ -385,6 +388,18 @@ user.listed_turf = T user.client << output("[url_encode(json_encode(T.name))];", "statbrowser:create_listedturf") +///The base proc of when something is right clicked on when alt is held - generally use alt_click_secondary instead +/atom/proc/alt_click_on_secondary(atom/A) + . = SEND_SIGNAL(src, COMSIG_MOB_ALTCLICKON_SECONDARY, A) + if(. & COMSIG_MOB_CANCEL_CLICKON) + return + A.alt_click_secondary(src) + +///The base proc of when something is right clicked on when alt is held +/atom/proc/alt_click_secondary(mob/user) + if(SEND_SIGNAL(src, COMSIG_CLICK_ALT_SECONDARY, user) & COMPONENT_CANCEL_CLICK_ALT_SECONDARY) + return + /// Use this instead of [/mob/proc/AltClickOn] where you only want turf content listing without additional atom alt-click interaction /atom/proc/AltClickNoInteract(mob/user, atom/A) var/turf/T = get_turf(A) diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm index a5aee2b6ad7..52107da7317 100644 --- a/code/modules/reagents/chemistry/holder.dm +++ b/code/modules/reagents/chemistry/holder.dm @@ -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() diff --git a/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm b/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm new file mode 100644 index 00000000000..c3e960e7830 --- /dev/null +++ b/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm @@ -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, " 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, "You add [beaker] to [src].") + 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, " 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, "You add [beaker] to [src].") + updateUsrDialog() + update_appearance() + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/chem_mass_spec/AltClick(mob/living/user) + . = ..() + if(processing_reagents) + to_chat(user, " 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, " 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 diff --git a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm index 6ea9292457f..382dd4463c9 100644 --- a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm +++ b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm @@ -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 diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm index bc17d24d72e..16bf886e1ce 100644 --- a/code/modules/reagents/chemistry/reagents.dm +++ b/code/modules/reagents/chemistry/reagents.dm @@ -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 . = ..() diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm index ce3c845961c..f2c17b0cb1b 100644 --- a/code/modules/reagents/reagent_containers.dm +++ b/code/modules/reagents/reagent_containers.dm @@ -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 diff --git a/icons/obj/chemical.dmi b/icons/obj/chemical.dmi index 5260ae293e5..080fe6e554a 100644 Binary files a/icons/obj/chemical.dmi and b/icons/obj/chemical.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 47e83ba0fec..04b37adec29 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3036,6 +3036,7 @@ #include "code\modules\reagents\chemistry\recipes.dm" #include "code\modules\reagents\chemistry\machinery\chem_dispenser.dm" #include "code\modules\reagents\chemistry\machinery\chem_heater.dm" +#include "code\modules\reagents\chemistry\machinery\chem_mass_spec.dm" #include "code\modules\reagents\chemistry\machinery\chem_master.dm" #include "code\modules\reagents\chemistry\machinery\chem_recipe_debug.dm" #include "code\modules\reagents\chemistry\machinery\chem_synthesizer.dm" diff --git a/tgui/packages/tgui/interfaces/MassSpec.js b/tgui/packages/tgui/interfaces/MassSpec.js new file mode 100644 index 00000000000..01e6df034d2 --- /dev/null +++ b/tgui/packages/tgui/interfaces/MassSpec.js @@ -0,0 +1,290 @@ +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, context) => { + const { act, data } = useBackend(context); + const { + processing, + lowerRange, + upperRange, + graphUpperRange, + graphLowerRange, + eta, + beaker1CurrentVolume, + beaker2CurrentVolume, + beaker1MaxVolume, + beaker2MaxVolume, + peakHeight, + beaker1, + beaker2, + beaker1Contents = [], + beaker2Contents = [], + } = data; + + const centerValue = (lowerRange + upperRange) / 2; + + return ( + + + {!!processing && ( + + + {' Purifying... '+round(eta)+"s"} + + )} +
act('activate')} /> + }> + {beaker1Contents.length && ( + + ) || ( + + Please insert an input beaker with reagents! + + )} +
+ +
+ {!!beaker1MaxVolume && ( + + {beaker1CurrentVolume} / {beaker1MaxVolume} units + + )} +
+
+ {!!beaker2MaxVolume && ( + + {beaker2CurrentVolume} / {beaker2MaxVolume} units + + )} +
+
+
+ ); +}; + +const BeakerMassProfile = props => { + const { + loaded, + details, + beaker = [], + } = props; + + return ( + + + {!loaded && ( + + No beaker loaded. + + ) || beaker.length === 0 && ( + + Beaker is empty. + + ) || ( + + + + Reagent + + + Volume + + + Mass + + + Type + + {!!details && ( + + Results + + )} + + {beaker.map(reagent => ( + + + {reagent.name} + + + {reagent.volume} + + + {reagent.mass} + + + ▮{reagent.type} + + {!!details && ( + + {reagent.log} + + )} + + ))} +
+ )} +
+ ); +}; + +const MassSpectroscopy = (props, context) => { + const { act, data } = useBackend(context); + const { + lowerRange, + centerValue, + upperRange, + graphUpperRange, + graphLowerRange, + maxAbsorbance, + reagentPeaks = [], + } = props; + + const deltaRange = graphUpperRange - graphLowerRange; + + const graphIncrement = deltaRange * 0.2; + + return ( + <> + + + + {/* x axis*/} + Mass (g) + {graphLowerRange} + {round(graphLowerRange + (graphIncrement), 1)} + {round(graphLowerRange + (graphIncrement * 2), 1)} + {round(graphLowerRange + (graphIncrement * 3), 1)} + {round(graphLowerRange + (graphIncrement * 4), 1)} + {graphUpperRange} + {/* y axis*/} + {round(maxAbsorbance, 1)} + {round(maxAbsorbance * 0.8, 1)} + {round(maxAbsorbance * 0.6, 1)} + {round(maxAbsorbance * 0.4, 1)} + {round(maxAbsorbance * 0.2, 1)} + 0 + + + Absorbance (AU) + + + {reagentPeaks.map(peak => ( + // Triangle peak + + ))} + + + + + + + + round(value)} + width={(centerValue/graphUpperRange)*400+"px"} + value={lowerRange} + minValue={graphLowerRange} + maxValue={centerValue} + color={"invisible"} + onDrag={(e, value) => act('leftSlider', { + value: 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, + })} > + {" "} + + + round(value)} + width={400+"px"} + minValue={graphLowerRange + 1} + maxValue={graphUpperRange - 1} + color={"invisible"} + onDrag={(e, value) => act('centerSlider', { + value: value, + })} > + {" "} + + + + + ); +};