mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2026-08-29 16:19:36 +01:00
Merge pull request #5050 from Darlantanis/ChemistrySynthesizer
Programmable Automated Chemical Synthesizer
This commit is contained in:
@@ -49,7 +49,8 @@
|
||||
/obj/structure/frame,
|
||||
/obj/machinery/radiocarbon_spectrometer,
|
||||
/obj/machinery/portable_atmospherics/powered/reagent_distillery,
|
||||
/obj/machinery/feeder
|
||||
/obj/machinery/feeder,
|
||||
/obj/machinery/chemical_synthesizer //CHOMPedit,
|
||||
)
|
||||
//CHOMP Addition for feeder in the above list. I am paranoid about comments within lists so this is outside.
|
||||
|
||||
|
||||
@@ -73256,6 +73256,7 @@
|
||||
/turf/simulated/floor/tiled/white,
|
||||
/area/medical/medbay)
|
||||
"tpy" = (
|
||||
/obj/machinery/chemical_synthesizer,
|
||||
/obj/machinery/hologram/holopad,
|
||||
/obj/machinery/atmospherics/pipe/simple/hidden/supply{
|
||||
dir = 10
|
||||
|
||||
@@ -0,0 +1,637 @@
|
||||
#define SYNTHESIZER_MAX_CARTRIDGES 40
|
||||
#define SYNTHESIZER_MAX_RECIPES 20
|
||||
#define SYNTHESIZER_MAX_QUEUE 40
|
||||
#define RECIPE_MAX_STRING 160
|
||||
#define RECIPE_MAX_STEPS 16
|
||||
|
||||
// Recipes are stored as a list which alternates between chemical id's and volumes to add, e.g. 1 = 'Carbon', 2 = 20, 3 = 'Silicon', 4 = 20
|
||||
/obj/machinery/chemical_synthesizer
|
||||
name = "chemical synthesizer"
|
||||
desc = "A programmable machine capable of automatically synthesizing medicine."
|
||||
icon = 'modular_chomp/icons/obj/chemical_ch.dmi'
|
||||
icon_state = "synth_idle_bottle"
|
||||
|
||||
use_power = USE_POWER_IDLE
|
||||
power_channel = EQUIP
|
||||
idle_power_usage = 100
|
||||
active_power_usage = 150
|
||||
anchored = TRUE
|
||||
unacidable = TRUE
|
||||
density = TRUE
|
||||
panel_open = TRUE
|
||||
|
||||
var/busy = FALSE
|
||||
var/production_mode = FALSE // Toggle between click-step input and comma-delineated text input for creating recipes.
|
||||
var/use_catalyst = TRUE // Determines whether or not the catalyst will be added to reagents while processing a recipe.
|
||||
var/delay_modifier = 4 // This is multiplied by the volume of a step to determine how long each step takes. Bigger volume = slower.
|
||||
var/obj/item/weapon/reagent_containers/glass/catalyst = null // This is where the user adds catalyst. Usually phoron.
|
||||
|
||||
var/list/recipes = list() // This holds chemical recipes up to a maximum determined by SYNTHESIZER_MAX_RECIPES. Two-dimensional.
|
||||
var/list/queue = list() // This holds the recipe id's for queued up recipes.
|
||||
var/list/catalyst_ids = list() // This keeps track of the chemicals in the catalyst to remove before bottling.
|
||||
var/list/cartridges = list() // Associative, label -> cartridge
|
||||
|
||||
var/list/spawn_cartridges = list(
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/hydrogen,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/lithium,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/carbon,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/nitrogen,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/oxygen,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/fluorine,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/sodium,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/aluminum,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/silicon,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/phosphorus,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/sulfur,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/chlorine,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/potassium,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/iron,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/copper,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/mercury,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/radium,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/water,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/ethanol,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/sugar,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/sacid,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/tungsten,
|
||||
/obj/item/weapon/reagent_containers/chem_disp_cartridge/calcium
|
||||
)
|
||||
|
||||
var/_recharge_reagents = TRUE
|
||||
var/process_tick = 0
|
||||
var/list/dispense_reagents = list(
|
||||
"hydrogen", "lithium", "carbon", "nitrogen", "oxygen", "fluorine", "sodium",
|
||||
"aluminum", "silicon", "phosphorus", "sulfur", "chlorine", "potassium", "iron",
|
||||
"copper", "mercury", "radium", "water", "ethanol", "sugar", "sacid", "tungsten", "calcium"
|
||||
)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/Initialize()
|
||||
. = ..()
|
||||
// Create the reagents datum which will act as the machine's reaction vessel.
|
||||
create_reagents(600)
|
||||
catalyst = new /obj/item/weapon/reagent_containers/glass/beaker(src)
|
||||
|
||||
if(spawn_cartridges)
|
||||
for(var/type in spawn_cartridges)
|
||||
add_cartridge(new type(src))
|
||||
panel_open = FALSE
|
||||
|
||||
var/obj/item/weapon/paper/P = new /obj/item/weapon/paper(get_turf(src))
|
||||
P.name = "Synthesizer Instructions"
|
||||
P.desc = "A photocopy of a handwritten note."
|
||||
P.info = {"Hello there! This device is a new NanoTrasen product currently being shipped to select facilities \
|
||||
for internal testing! We haven't finished the instruction manual yet so each unit shipped with this pamphlet \
|
||||
(I really hope you can read my handwriting). This machine is a programmable chemical synthesizer which, if used \
|
||||
correctly, will allow you to queue up some recipes and go work on something else while the medicine manufactures. \
|
||||
It's slower than doing things by hand but it also keeps your hands free! And yes, it bottles automatically. \
|
||||
<BR><BR>To get started, you need to program some recipes. The machine has two modes for this: tutorial and production. \
|
||||
Tutorial is intended to teach you how recipes work, or to create recipes for later use with production mode. This one \
|
||||
should be self-explanatory, just follow the prompts. Production mode allows you to rapidly import recipes in CSV format. \
|
||||
If I've lost you, just keep reading; once you give it a name and tell the machine how many steps are in the recipe, just \
|
||||
input the recipe as a comma-separated string of chemical names and volumes to be added, like "Chem1,10,Chem2,20,... \
|
||||
<BR><BR>If that still doesn't make sense, I've included an example for Dylovene at the bottom. Also, don't include \
|
||||
catalyst reagents in the recipe, read below for that part. Also also, remember that chemical names are case sensitive and \
|
||||
cartridge names are usually Capitalized Like These Words (AND FOR THE LOVE OF GOD KEVIN, STOP CHANGING THE NAMES ON THE \
|
||||
LABELS WHEN THE CHEMISTS AREN'T LOOKING IT ISN'T FUNNY ANYMORE). \
|
||||
<BR><BR>Next important concept is the catalyst, intended for catalyst reagents (usually phoron). When the catalyst option is \
|
||||
enabled, whatever is in the catalyst bottle gets added to the reaction chamber before synthesis begins. When the \
|
||||
recipe is done, it extracts the catalyst, bottles whatever you made, then adds the catalyst back before starting \
|
||||
the next recipe in the queue. It's up to you to make sure no unwanted side reactions happen, and yes, this means \
|
||||
you cannot queue up catalyst recipes with recipes ruined by the catalyst. Maybe add "NO CAT" or something to the \
|
||||
name for recipes like that? \
|
||||
<BR><BR>And that's the really important stuff. Remember you can export recipes for later shifts, just copy the \
|
||||
output into your PDA or something. Oh, and stalling. Say you're missing a cartridge or the vessel is full (the \
|
||||
capacity is [src.reagents.maximum_volume]) or you press the emergency stop button. The machine will stall, \
|
||||
clearing the temporary memory. To get it started again, you just need to empty the vessel. Anyway, here's \
|
||||
example recipe. \
|
||||
<BR><BR> Name: Dylovene (60u) \
|
||||
<BR> Number of steps: 3 \
|
||||
<BR> Recipe string: Silicon,20,Nitrogen,20,Potassium,20"}
|
||||
|
||||
/obj/machinery/chemical_synthesizer/examine(mob/user)
|
||||
. = ..()
|
||||
if(panel_open)
|
||||
. += "It has [cartridges.len] cartridges installed, and has space for [SYNTHESIZER_MAX_CARTRIDGES - cartridges.len] more."
|
||||
|
||||
/obj/machinery/chemical_synthesizer/power_change()
|
||||
. = ..()
|
||||
update_icon()
|
||||
|
||||
/obj/machinery/chemical_synthesizer/update_icon()
|
||||
underlays.Cut()
|
||||
if(stat & BROKEN)
|
||||
icon_state = "synth_broken"
|
||||
return
|
||||
if(stat & NOPOWER)
|
||||
icon_state = "synth_off"
|
||||
return
|
||||
if(!busy)
|
||||
if(catalyst)
|
||||
icon_state = "synth_idle_bottle"
|
||||
else
|
||||
icon_state = "synth_idle"
|
||||
else
|
||||
icon_state = "synth_working"
|
||||
if(catalyst) // All underlay icon_states requires the catalyst bottle to be present, so this works as a check.
|
||||
if(catalyst.reagents.reagent_list.len)
|
||||
var/image/cat_filling = image(icon, src, "synth_catalyst", -1)
|
||||
cat_filling.color = catalyst.reagents.get_color()
|
||||
underlays += cat_filling
|
||||
if(src.reagents.reagent_list.len)
|
||||
var/image/ves_filling = image(icon, src, "synth_vessel", -2)
|
||||
ves_filling.color = src.reagents.get_color()
|
||||
underlays += ves_filling
|
||||
|
||||
/obj/machinery/chemical_synthesizer/proc/add_cartridge(obj/item/weapon/reagent_containers/chem_disp_cartridge/C, mob/user)
|
||||
if(!panel_open)
|
||||
if(user)
|
||||
to_chat(user, "<span class='warning'>\The panel is locked!</span>")
|
||||
return
|
||||
|
||||
if(!istype(C))
|
||||
if(user)
|
||||
to_chat(user, "<span class='warning'>\The [C] will not fit in \the [src]!</span>")
|
||||
return
|
||||
|
||||
if(cartridges.len >= SYNTHESIZER_MAX_CARTRIDGES)
|
||||
if(user)
|
||||
to_chat(user, "<span class='warning'>\The [src] does not have any slots open for \the [C] to fit into!</span>")
|
||||
return
|
||||
|
||||
if(!C.label)
|
||||
if(user)
|
||||
to_chat(user, "<span class='warning'>\The [C] does not have a label!</span>")
|
||||
return
|
||||
|
||||
if(cartridges[C.label])
|
||||
if(user)
|
||||
to_chat(user, "<span class='warning'>\The [src] already contains a cartridge with that label!</span>")
|
||||
return
|
||||
|
||||
if(user)
|
||||
user.drop_from_inventory(C)
|
||||
to_chat(user, "<span class='notice'>You add \the [C] to \the [src].</span>")
|
||||
|
||||
C.loc = src
|
||||
cartridges[C.label] = C
|
||||
cartridges = sortAssoc(cartridges)
|
||||
SStgui.update_uis(src)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/proc/remove_cartridge(label)
|
||||
. = cartridges[label]
|
||||
cartridges -= label
|
||||
SStgui.update_uis(src)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/attackby(obj/item/weapon/W, mob/user)
|
||||
// Why do so many people code in wrenching when there's already a proc for it?
|
||||
if(!busy && default_unfasten_wrench(user, W, 40))
|
||||
return
|
||||
|
||||
if(istype(W, /obj/item/weapon/reagent_containers/chem_disp_cartridge))
|
||||
add_cartridge(W, user)
|
||||
return
|
||||
|
||||
// But we won't use the screwdriver proc because chem dispenser behavior.
|
||||
if(panel_open && W.is_screwdriver())
|
||||
var/label = tgui_input_list(user, "Which cartridge would you like to remove?", "Chemical Synthesizer", cartridges)
|
||||
if(!label)
|
||||
return
|
||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = remove_cartridge(label)
|
||||
if(C)
|
||||
to_chat(user, "<span class='notice'>You remove \the [C] from \the [src].</span>")
|
||||
C.loc = loc
|
||||
playsound(src, W.usesound, 50, 1)
|
||||
return
|
||||
|
||||
// We don't need a busy check here as the catalyst slot must be occupied for the machine to function.
|
||||
if(istype(W, /obj/item/weapon/reagent_containers/glass))
|
||||
if(catalyst)
|
||||
to_chat(user, "<span class='warning'>There is already \a [catalyst] in \the [src] catalyst slot!</span>")
|
||||
return
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
to_chat(user, "<span class='warning'>The clamp will not secure the catalyst while the machine is down!</span>")
|
||||
return
|
||||
|
||||
var/obj/item/weapon/reagent_containers/RC = W
|
||||
|
||||
if(!RC.is_open_container())
|
||||
to_chat(user, "<span class='warning'>You don't see how \the [src] could extract reagents from \the [RC].</span>")
|
||||
return
|
||||
|
||||
catalyst = RC
|
||||
user.drop_from_inventory(RC)
|
||||
RC.loc = src
|
||||
to_chat(user, "<span class='notice'>You set \the [RC] on \the [src].</span>")
|
||||
update_icon()
|
||||
return
|
||||
|
||||
return ..()
|
||||
|
||||
// More stolen chemical_dispenser code.
|
||||
/obj/machinery/chemical_synthesizer/process()
|
||||
if(!_recharge_reagents)
|
||||
return
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
return
|
||||
if(--process_tick <= 0)
|
||||
process_tick = 15
|
||||
. = 0
|
||||
for(var/id in dispense_reagents)
|
||||
var/datum/reagent/R = SSchemistry.chemical_reagents[id]
|
||||
if(!R)
|
||||
stack_trace("[src] at [x],[y],[z] failed to find reagent '[id]'!")
|
||||
dispense_reagents -= id
|
||||
continue
|
||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[R.name]
|
||||
if(C && C.reagents.total_volume < C.reagents.maximum_volume)
|
||||
var/to_restore = min(C.reagents.maximum_volume - C.reagents.total_volume, 5)
|
||||
use_power(to_restore * 500)
|
||||
C.reagents.add_reagent(id, to_restore)
|
||||
. = 1
|
||||
if(.)
|
||||
SStgui.update_uis(src)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/tgui_interact(mob/user, datum/tgui/ui = null)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "ChemSynthesizer", name)
|
||||
ui.open()
|
||||
|
||||
/obj/machinery/chemical_synthesizer/tgui_data(mob/user)
|
||||
var/list/data = list()
|
||||
|
||||
data["busy"] = busy
|
||||
data["production_mode"] = production_mode
|
||||
data["panel_open"] = panel_open
|
||||
data["use_catalyst"] = use_catalyst
|
||||
|
||||
var/list/tmp_queue = list()
|
||||
for(var/i = 1, i <= queue.len, i++)
|
||||
tmp_queue.Add(list(list("name" = queue[i], "index" = i))) // Thanks byond
|
||||
data["queue"] = tmp_queue
|
||||
|
||||
// Convert the recipes list into an array of strings. The UI does not need the associative list attached to each string.
|
||||
var/list/tmp_recipes = list()
|
||||
for(var/i = 1, i <= recipes.len, i++)
|
||||
tmp_recipes.Add(list(list("name" = recipes[i])))
|
||||
data["recipes"] = tmp_recipes
|
||||
|
||||
|
||||
// Read data from the reaction vessel.
|
||||
var/list/vessel_reagents_list = list()
|
||||
data["rxn_vessel"] = vessel_reagents_list
|
||||
for(var/datum/reagent/R in src.reagents.reagent_list)
|
||||
vessel_reagents_list[++vessel_reagents_list.len] = list("name" = R.name, "volume" = R.volume, "description" = R.description, "id" = R.id)
|
||||
|
||||
// Read data from the catalyst, if present.
|
||||
data["catalyst"] = catalyst ? 1 : 0
|
||||
if(catalyst)
|
||||
var/list/catalyst_reagents_list = list()
|
||||
data["catalyst_reagents"] = catalyst_reagents_list
|
||||
for(var/datum/reagent/R in catalyst.reagents.reagent_list)
|
||||
catalyst_reagents_list[++catalyst_reagents_list.len] = list("name" = R.name, "volume" = R.volume, "description" = R.description, "id" = R.id)
|
||||
|
||||
if(catalyst)
|
||||
data["catalystCurrentVolume"] = catalyst.reagents.total_volume
|
||||
data["catalystMaxVolume"] = catalyst.reagents.maximum_volume
|
||||
else
|
||||
data["catalystCurrentVolume"] = null
|
||||
data["catalystMaxVolume"] = null
|
||||
|
||||
var/chemicals[0]
|
||||
for(var/label in cartridges)
|
||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
||||
chemicals.Add(list(list("title" = label, "id" = label, "amount" = C.reagents.total_volume))) // list in a list because Byond merges the first list
|
||||
data["chemicals"] = chemicals
|
||||
|
||||
return data
|
||||
|
||||
/obj/machinery/chemical_synthesizer/tgui_act(action, params)
|
||||
if(..())
|
||||
return TRUE
|
||||
|
||||
. = TRUE
|
||||
switch(action)
|
||||
if("start_queue")
|
||||
// Start up the queue.
|
||||
if(!busy)
|
||||
start_queue(usr)
|
||||
if("rem_queue")
|
||||
// Remove a single entry from the queue. Sanity checks also prevent removing the first entry if the machine is busy though UI should already prevent that.
|
||||
var/index = text2num(params["q_index"])
|
||||
if(!isnum(index) || !ISINTEGER(index) || !istype(queue) || (index<1 || index>length(queue) || (busy && index == 1)))
|
||||
return
|
||||
queue -= queue[index]
|
||||
if("clear_queue")
|
||||
// Remove all entries from the queue except the currently processing recipe.
|
||||
var/confirm = alert(usr, "Are you sure you want to clear the running queue?", "Confirm", "No", "Yes")
|
||||
if(confirm == "Yes")
|
||||
if(busy)
|
||||
// Oh no, I've broken code convention to remove all entries but the first.
|
||||
for(var/i = queue.len, i >= 2, i--)
|
||||
queue -= queue[i]
|
||||
else
|
||||
queue = list()
|
||||
if("eject_catalyst")
|
||||
// Removes the catalyst bottle from the machine.
|
||||
if(!busy && catalyst)
|
||||
catalyst.forceMove(get_turf(src))
|
||||
catalyst = null
|
||||
update_icon()
|
||||
if("toggle_catalyst")
|
||||
// Decides if the machine uses the catalyst.
|
||||
if(!busy)
|
||||
use_catalyst = !use_catalyst
|
||||
if("emergency_stop")
|
||||
// Stops everything if that's desirable for some reason.
|
||||
if(busy)
|
||||
var/confirm = alert(usr, "Are you sure you want to stall the machine?", "Confirm", "Yes", "No")
|
||||
if(confirm == "Yes")
|
||||
stall()
|
||||
if("bottle_product")
|
||||
// Bottles the reaction mixture if stalled.
|
||||
if(!busy)
|
||||
bottle_product()
|
||||
if("panel_toggle")
|
||||
// Opens/closes the panel.
|
||||
if(!busy)
|
||||
panel_open = !panel_open
|
||||
if("mode_toggle")
|
||||
// Toggles production mode.
|
||||
production_mode = !production_mode
|
||||
if("add_recipe")
|
||||
// Allows the user to add a recipe. Kinda vital for this machine to do anything useful.
|
||||
if(recipes.len >= SYNTHESIZER_MAX_RECIPES)
|
||||
to_chat(usr, "<span class='warning'>Maximum recipes exceeded!</span>")
|
||||
return
|
||||
if(!production_mode)
|
||||
babystep_recipe(usr)
|
||||
else
|
||||
import_recipe(usr)
|
||||
if("rem_recipe")
|
||||
// Allows the user to remove recipes while the machine is idle.
|
||||
if(!busy)
|
||||
var/confirm = alert(usr, "Are you sure you want to remove this recipe?", "Confirm", "No", "Yes")
|
||||
if(confirm == "Yes")
|
||||
var/index = params["rm_index"]
|
||||
if(index in recipes)
|
||||
recipes.Remove(list(index)) // Fuck off Byond.
|
||||
else
|
||||
to_chat(usr, "<span class='warning'>You cannot remove recipes while the machine is running!</span>")
|
||||
if("exp_recipe")
|
||||
// Allows the user to export recipes to chat formatted for easy importing.
|
||||
var/index = params["exp_index"]
|
||||
export_recipe(usr, index)
|
||||
if("add_queue")
|
||||
// Adds recipes to the queue.
|
||||
if(queue.len >= SYNTHESIZER_MAX_QUEUE)
|
||||
to_chat(usr, "<span class='warning'>Synthesizer queue full!</span>")
|
||||
return
|
||||
var/index = params["qa_index"]
|
||||
// If you forgot, this is a string returned by the user pressing the "add to queue" button on a recipe.
|
||||
if(index in recipes)
|
||||
queue[++queue.len] = index
|
||||
|
||||
add_fingerprint(usr)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/attack_ghost(mob/user)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
return
|
||||
tgui_interact(user)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/attack_ai(mob/user)
|
||||
attack_hand(user)
|
||||
|
||||
/obj/machinery/chemical_synthesizer/attack_hand(mob/user)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
return
|
||||
tgui_interact(user)
|
||||
|
||||
// This proc is lets users create recipes step-by-step and exports a comma delineated list to chat. It's intended to teach how to use the machine.
|
||||
/obj/machinery/chemical_synthesizer/proc/babystep_recipe(mob/user)
|
||||
var/rec_name = sanitizeSafe(input(user, "Name your recipe. Consider including the output volume.", "Recipe naming", null) as text, MAX_NAME_LEN)
|
||||
if(!rec_name || (rec_name in recipes)) // Code requires each recipe to have a unique name.
|
||||
to_chat(user, "Please provide a unique recipe name!")
|
||||
return
|
||||
|
||||
var/steps = 2 * CLAMP(round(input(user, "How many steps does your recipe contain (16 max)?", "Steps", null) as num), 0, RECIPE_MAX_STEPS) // Round to get a whole integer, clamp to ensure proper range.
|
||||
if(!steps)
|
||||
to_chat(user, "Please input a valid number of steps!")
|
||||
return
|
||||
|
||||
var/list/new_rec = list() // This holds the actual recipe.
|
||||
for(var/i = 1, i < steps, i += 2) // For the user, 1 step is both text and volume. For list arithmetic, that's 2 steps.
|
||||
var/label = tgui_input_list(user, "Which chemical would you like to use?", "Chemical Synthesizer", cartridges)
|
||||
if(!label)
|
||||
to_chat(user, "Please select a chemical!")
|
||||
return
|
||||
new_rec[++new_rec.len] = label // Add the reagent ID.
|
||||
var/amount = CLAMP(round(input(user, "How much of the chemical would you like to add?", "Volume", null) as num), 0, src.reagents.maximum_volume)
|
||||
if(!amount)
|
||||
to_chat(user, "Please select a volume!")
|
||||
return
|
||||
new_rec[++new_rec.len] = amount // Add the amount of reagent.
|
||||
|
||||
recipes[rec_name] = new_rec
|
||||
SStgui.update_uis(src)
|
||||
export_recipe(user, rec_name) // Now export the recipe to the user's chatbox formatted for import_recipe().
|
||||
return
|
||||
|
||||
// This proc allows users to copy-paste a comma delineated list to create a recipe. The recipe will cause a stall() if formatted incorrectly.
|
||||
/obj/machinery/chemical_synthesizer/proc/import_recipe(mob/user)
|
||||
var/rec_name = sanitizeSafe(input(user, "Name your recipe. Consider including the output volume.", "Recipe naming", null) as text, MAX_NAME_LEN)
|
||||
if(!rec_name || (rec_name in recipes)) // Code requires each recipe to have a unique name.
|
||||
to_chat(user, "Please provide a unique recipe name!")
|
||||
return
|
||||
|
||||
var/rec_input = input(user, "Input your recipe as 'Chem1,vol1,Chem2,vol2,...'", "Import recipe", null)
|
||||
if(!rec_input || (length(rec_input) > RECIPE_MAX_STRING) || !findtext(rec_input, ",")) // The smallest possible recipe will contain 1 comma.
|
||||
to_chat(user, "Invalid input or recipe max length exceeded!")
|
||||
return
|
||||
|
||||
rec_input = trim(rec_input) // Sanitize.
|
||||
var/list/new_rec = list() // This holds the actual recipe.
|
||||
var/vol = FALSE // This tracks if the next step is a chemical name or a volume.
|
||||
var/index = findtext(rec_input, ",") // This tracks the delineation index in the user-provided string. Should never be null at this point.
|
||||
var/i = 1 // This tracks the index for new_rec, the actual list which gets added to recipes[rec_name].
|
||||
while(index) // Alternates between text strings and numbers. When false, the rest of rec_input is the final step.
|
||||
new_rec[++new_rec.len] = copytext(rec_input, 1, index)
|
||||
if(vol)
|
||||
new_rec[i] = text2num(new_rec[i]) // If it's a volume step, convert to a number.
|
||||
vol = FALSE
|
||||
else
|
||||
vol = TRUE
|
||||
i++
|
||||
rec_input = copytext(rec_input, index + 1) // Trim previous substrings from rec_input.
|
||||
index = findtext(rec_input, ",")
|
||||
|
||||
if(rec_input) // The remainder of rec_input should be the final volume step of the recipe. The if() is a sanity check.
|
||||
new_rec[++new_rec.len] = text2num(rec_input)
|
||||
|
||||
recipes[rec_name] = new_rec // Finally, add the recipe to the recipes list.
|
||||
SStgui.update_uis(src)
|
||||
return
|
||||
|
||||
// This proc exports stored recipes to the user's chatbox formatted as a comma delineated list for use with import_recpe()
|
||||
/obj/machinery/chemical_synthesizer/proc/export_recipe(mob/user, rec_name)
|
||||
var/list/export = recipes[rec_name]
|
||||
if(!export)
|
||||
return
|
||||
var/display_txt = export.Join(",") // This converts the entire list into a CSV string.
|
||||
to_chat(user, "[display_txt]")
|
||||
|
||||
// This proc handles adding the catalyst starting the synthesizer's queue.
|
||||
/obj/machinery/chemical_synthesizer/proc/start_queue(mob/user)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
return
|
||||
|
||||
if(!queue)
|
||||
to_chat(user, "You can't start an empty queue!")
|
||||
return
|
||||
|
||||
if(!catalyst)
|
||||
to_chat(user, "Place a bottle in the catalyst slot before starting the queue!")
|
||||
return
|
||||
|
||||
if(panel_open)
|
||||
to_chat(user, "Close the panel before starting the queue!")
|
||||
return
|
||||
|
||||
if(reagents.total_volume)
|
||||
to_chat(user, "Empty the reaction vessel before starting the queue!")
|
||||
return
|
||||
|
||||
busy = TRUE
|
||||
use_power = USE_POWER_ACTIVE
|
||||
if(use_catalyst)
|
||||
// Populate the list of catalyst chems. This is important when it's time to bottle_product().
|
||||
for(var/datum/reagent/chem in catalyst.reagents.reagent_list)
|
||||
catalyst_ids += chem.id
|
||||
|
||||
// Transfer the catalyst to the synthesizer's reagent holder.
|
||||
catalyst.reagents.trans_to_holder(src.reagents, catalyst.reagents.total_volume)
|
||||
|
||||
// Start the first recipe in the queue, starting with step 1.
|
||||
update_icon()
|
||||
follow_recipe(queue[1], 1)
|
||||
|
||||
|
||||
// This proc controls the timing for each step in a reaction. Step is the index for the current chem of our recipe, step + 1 is the volume of said chem.
|
||||
/obj/machinery/chemical_synthesizer/proc/follow_recipe(var/r_id, var/step as num)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
stall()
|
||||
return
|
||||
|
||||
if(!step)
|
||||
step = 1
|
||||
|
||||
// The time between each step is the volume required by a step multiplied by the delay_modifier (in ticks/deciseconds).
|
||||
addtimer(CALLBACK(src, .proc/perform_reaction, r_id, step), recipes[r_id][step + 1] * delay_modifier)
|
||||
|
||||
// This proc carries out the actual steps in each reaction.
|
||||
/obj/machinery/chemical_synthesizer/proc/perform_reaction(var/r_id, var/step as num)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
stall()
|
||||
return
|
||||
|
||||
//Let's store these as temporary variables to make the code more readable.
|
||||
var/label = recipes[r_id][step]
|
||||
var/quantity = recipes[r_id][step+1]
|
||||
|
||||
// If we're missing a cartridge somehow or lack space for the next step, stall. It's now up to the chemist to fix this.
|
||||
if(!cartridges[label])
|
||||
visible_message("<span class='warning'>The [src] beeps loudly, flashing a 'cartridge missing' error!</span>", "You hear loud beeping!")
|
||||
playsound(src, 'sound/weapons/smg_empty_alarm.ogg', 40)
|
||||
stall()
|
||||
return
|
||||
|
||||
if(quantity > reagents.get_free_space())
|
||||
visible_message("<span class='warning'>The [src] beeps loudly, flashing a 'maximum volume exceeded' error!</span>", "You hear loud beeping!")
|
||||
playsound(src, 'sound/weapons/smg_empty_alarm.ogg', 40)
|
||||
stall()
|
||||
return
|
||||
|
||||
// If there isn't enough reagent left for this step, try again in a minute.
|
||||
var/obj/item/weapon/reagent_containers/chem_disp_cartridge/C = cartridges[label]
|
||||
if(quantity > C.reagents.total_volume)
|
||||
visible_message("<span class='notice'>The [src] flashes an 'insufficient reagents' warning.</span>")
|
||||
addtimer(CALLBACK(src, .proc/perform_reaction, r_id, step), 1 MINUTE)
|
||||
return
|
||||
|
||||
// After all this mess of code, we reach the line where the magic happens.
|
||||
C.reagents.trans_to_holder(src.reagents, quantity)
|
||||
update_icon() // Update underlays.
|
||||
playsound(src, 'modular_chomp/sound/machines/HPLC_binary_pump.ogg', 15, 1)
|
||||
|
||||
// Advance to the next step in the recipe. If this is outside of the recipe's index, we're finished. Otherwise, proceed to next step.
|
||||
step += 2
|
||||
var/list/tmp = recipes[r_id]
|
||||
if(step > tmp.len)
|
||||
|
||||
// First extract the catalyst(s), if any remain.
|
||||
if(use_catalyst)
|
||||
for(var/chem in catalyst_ids)
|
||||
var/amount = reagents.get_reagent_amount(chem)
|
||||
reagents.trans_id_to(catalyst, chem, amount)
|
||||
|
||||
// Add a delay of 1 tick per unit of reagent. Clear the catalyst_ids.
|
||||
catalyst_ids = list()
|
||||
var/delay = reagents.total_volume
|
||||
update_icon() // Update the icon first to remove underlays, then switch to the new icon_state.
|
||||
icon_state = "synth_finished"
|
||||
addtimer(CALLBACK(src, .proc/bottle_product, r_id), delay)
|
||||
|
||||
else
|
||||
follow_recipe(r_id, step)
|
||||
|
||||
// Now that we're done, bottle up the product.
|
||||
/obj/machinery/chemical_synthesizer/proc/bottle_product(var/r_id)
|
||||
if(stat & (BROKEN|NOPOWER))
|
||||
stall()
|
||||
return
|
||||
|
||||
if(!r_id)
|
||||
r_id = "[reagents.get_master_reagent_name()]"
|
||||
|
||||
while(reagents.total_volume)
|
||||
var/obj/item/weapon/reagent_containers/glass/bottle/B = new(src.loc)
|
||||
B.name = "[r_id] bottle"
|
||||
B.pixel_x = rand(-7, 7) // random position
|
||||
B.pixel_y = rand(-7, 7)
|
||||
B.icon_state = "bottle-4"
|
||||
reagents.trans_to_obj(B, min(reagents.total_volume, MAX_UNITS_PER_BOTTLE))
|
||||
B.update_icon()
|
||||
|
||||
// Sanity check when manual bottling is triggered.
|
||||
if(queue.len)
|
||||
queue -= queue[1]
|
||||
|
||||
// If the queue is now empty, we're done. Otherwise, re-add catalyst and proceed to the next recipe.
|
||||
if(queue.len)
|
||||
if(use_catalyst)
|
||||
for(var/datum/reagent/chem in catalyst.reagents.reagent_list)
|
||||
catalyst_ids += chem.id
|
||||
catalyst.reagents.trans_to_holder(src.reagents, catalyst.reagents.total_volume)
|
||||
update_icon()
|
||||
follow_recipe(queue[1], 1)
|
||||
|
||||
else
|
||||
busy = FALSE
|
||||
use_power = USE_POWER_IDLE
|
||||
queue = list()
|
||||
update_icon()
|
||||
|
||||
|
||||
// What happens to the synthesizer if it breaks or loses power in the middle of running. Chemists must fix things manually.
|
||||
/obj/machinery/chemical_synthesizer/proc/stall()
|
||||
busy = FALSE
|
||||
use_power = USE_POWER_IDLE
|
||||
queue = list()
|
||||
catalyst_ids = list()
|
||||
update_icon()
|
||||
|
||||
#undef SYNTHESIZER_MAX_CARTRIDGES
|
||||
#undef SYNTHESIZER_MAX_RECIPES
|
||||
#undef SYNTHESIZER_MAX_QUEUE
|
||||
#undef RECIPE_MAX_STRING
|
||||
#undef RECIPE_MAX_STEPS
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
@@ -0,0 +1,265 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, Flex, LabeledList, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { BeakerContents } from './common/BeakerContents';
|
||||
|
||||
export const ChemSynthesizer = (props, context) => {
|
||||
return (
|
||||
<Window width={1100} height={640} resizable>
|
||||
<Window.Content>
|
||||
<Flex height="100%">
|
||||
<Flex.Item grow={1} maxWidth="33%">
|
||||
<ChemSynthesizerQueueRecipes />
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<ChemSynthesizerChemicals />
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<ChemSynthesizerSettings />
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const ChemSynthesizerQueueRecipes = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { busy, use_catalyst, queue = [], recipes = [], production_mode } = data;
|
||||
|
||||
return (
|
||||
<Flex height="100%" width="100%" direction="column">
|
||||
<Flex.Item maxHeight="50%" grow={1} basis={0}>
|
||||
<Section
|
||||
height="100%"
|
||||
title="Queue"
|
||||
overflowY="scroll"
|
||||
buttons={
|
||||
<Fragment>
|
||||
<Button
|
||||
disabled={!!busy}
|
||||
color={use_catalyst ? 'green' : 'bad'}
|
||||
icon="wrench"
|
||||
tooltip="Enable/Disable the catalyst BEFORE starting the queue."
|
||||
content={use_catalyst ? 'Catalyst Active' : 'Catalyst Disabled'}
|
||||
onClick={() => act('toggle_catalyst')}
|
||||
/>
|
||||
<Button.Confirm
|
||||
disabled={!queue.length}
|
||||
color="bad"
|
||||
icon="minus-circle"
|
||||
tooltip="Clear Queue"
|
||||
onClick={() => act('clear_queue')}
|
||||
/>
|
||||
{!busy && (
|
||||
<Button disabled={!queue.length} icon="play" tooltip="Start Queue" onClick={() => act('start_queue')} />
|
||||
)}
|
||||
</Fragment>
|
||||
}>
|
||||
<LabeledList>
|
||||
{(queue.length &&
|
||||
queue.map((item) => {
|
||||
if (item.index === 1 && !!busy) {
|
||||
return (
|
||||
<LabeledList.Item label={item.name} labelColor="bad">
|
||||
{
|
||||
<Box>
|
||||
<Button disabled icon="trash">
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
</LabeledList.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LabeledList.Item label={item.name}>
|
||||
<Button
|
||||
icon="trash"
|
||||
onClick={() =>
|
||||
act('rem_queue', {
|
||||
q_index: item.index,
|
||||
})
|
||||
}>
|
||||
Delete
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})) || <Box m={1}>Queue Empty.</Box>}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
<Flex.Item maxHeight="50%" grow={1} basis={0}>
|
||||
<Section
|
||||
height="100%"
|
||||
title="Recipes"
|
||||
overflowY="scroll"
|
||||
buttons={
|
||||
<Button
|
||||
icon="plus"
|
||||
tooltip={production_mode ? 'Import Recipe' : 'Generate Recipe'}
|
||||
onClick={() => act('add_recipe')}
|
||||
/>
|
||||
}>
|
||||
<LabeledList>
|
||||
{(recipes.length &&
|
||||
recipes.map((item) => {
|
||||
return (
|
||||
<LabeledList.Item label={item.name}>
|
||||
<Button
|
||||
icon="plus"
|
||||
tooltip="Add to Queue"
|
||||
onClick={() =>
|
||||
act('add_queue', {
|
||||
qa_index: item.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="inbox"
|
||||
tooltip="Export Recipe"
|
||||
onClick={() =>
|
||||
act('exp_recipe', {
|
||||
exp_index: item.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
color="bad"
|
||||
icon="minus-circle"
|
||||
tooltip="Delete Recipe"
|
||||
disabled={!!busy}
|
||||
onClick={() =>
|
||||
act('rem_recipe', {
|
||||
rm_index: item.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})) || <Box m={1}>No recipes found.</Box>}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const ChemSynthesizerChemicals = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
busy,
|
||||
chemicals = [],
|
||||
rxn_vessel = [],
|
||||
catalyst,
|
||||
catalystCurrentVolume,
|
||||
catalystMaxVolume,
|
||||
catalyst_reagents = [],
|
||||
} = data;
|
||||
|
||||
const flexFillers = [];
|
||||
for (let i = 0; i < (chemicals.length + 1) % 3; i++) {
|
||||
flexFillers.push(true);
|
||||
}
|
||||
return (
|
||||
<Flex direction="column">
|
||||
<Section title="Cartridge Reagents" flexGrow="1">
|
||||
<Flex direction="row" wrap="wrap" height="100%" align="flex-start">
|
||||
{chemicals.map((c, i) => (
|
||||
<Flex.Item key={i} grow="1" m={0.2} basis="40%" height="20px">
|
||||
<Button
|
||||
icon="arrow-circle-down"
|
||||
width="100%"
|
||||
height="100%"
|
||||
align="flex-start"
|
||||
disabled={1}
|
||||
content={c.title + ' (' + c.amount + ')'}
|
||||
/>
|
||||
</Flex.Item>
|
||||
))}
|
||||
{flexFillers.map((_, i) => (
|
||||
<Flex.Item key={i} grow="1" basis="25%" height="20px" />
|
||||
))}
|
||||
</Flex>
|
||||
</Section>
|
||||
<Section title="Reaction Vessel">
|
||||
{rxn_vessel.length > 0 ? (
|
||||
<BeakerContents beakerLoaded beakerContents={rxn_vessel} />
|
||||
) : (
|
||||
<Box color="label">Vessel is empty.</Box>
|
||||
)}
|
||||
</Section>
|
||||
<Section
|
||||
title="Catalyst"
|
||||
flex="content"
|
||||
minHeight="25%"
|
||||
buttons={
|
||||
<Box>
|
||||
{!!catalyst && (
|
||||
<Box inline color="label" mr={2}>
|
||||
{catalystCurrentVolume} / {catalystMaxVolume} units
|
||||
</Box>
|
||||
)}
|
||||
<Button icon="eject" content="Eject" disabled={!catalyst || !!busy} onClick={() => act('eject_catalyst')} />
|
||||
</Box>
|
||||
}>
|
||||
<BeakerContents beakerLoaded={catalyst} beakerContents={catalyst_reagents} />
|
||||
</Section>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const ChemSynthesizerSettings = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { busy, production_mode, panel_open, rxn_vessel } = data;
|
||||
|
||||
return (
|
||||
<Flex height="100%" width="100%" direction="column">
|
||||
<Flex.Item height={0} grow={1}>
|
||||
<Section height="100%" title="Settings" overflowY="auto">
|
||||
<Flex direction="column">
|
||||
<Flex.Item>
|
||||
<Button
|
||||
color={production_mode ? 'green' : 'bad'}
|
||||
icon="wrench"
|
||||
content={production_mode ? 'Recipe mode: Import' : 'Recipe mode: Tutorial'}
|
||||
onClick={() => act('mode_toggle')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
disabled={!!busy}
|
||||
color={panel_open ? 'bad' : 'green'}
|
||||
icon="wrench"
|
||||
content={panel_open ? 'Panel Open' : 'Panel Closed'}
|
||||
onClick={() => act('panel_toggle')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
{!busy && (
|
||||
<Button
|
||||
disabled={!rxn_vessel.length}
|
||||
color="bad"
|
||||
icon="flask"
|
||||
tooltip="For emptying the reaction vessel if the machine stalls."
|
||||
content="Bottle Manually"
|
||||
onClick={() => act('bottle_product')}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
disabled={!busy}
|
||||
color="bad"
|
||||
icon="minus-circle"
|
||||
content="EMERGENCY STOP"
|
||||
onClick={() => act('emergency_stop')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -4559,6 +4559,7 @@
|
||||
#include "modular_chomp\code\modules\power\cells\device_cells.dm"
|
||||
#include "modular_chomp\code\modules\power\cells\esoteric_cells.dm"
|
||||
#include "modular_chomp\code\modules\power\cells\power_cells.dm"
|
||||
#include "modular_chomp\code\modules\reagents\machinery\dispenser\chem_synthesizer_ch.dm"
|
||||
#include "modular_chomp\code\modules\reagents\reactions\instant\drinks.dm"
|
||||
#include "modular_chomp\code\modules\reagents\reagents\food_drinks.dm"
|
||||
#include "modular_chomp\code\modules\research\designs\power_cells.dm"
|
||||
|
||||
Reference in New Issue
Block a user