diff --git a/code/__DEFINES/achievements.dm b/code/__DEFINES/achievements.dm
index 7a7bf3489e56bc..da640d795e4d06 100644
--- a/code/__DEFINES/achievements.dm
+++ b/code/__DEFINES/achievements.dm
@@ -31,6 +31,7 @@
#define MEDAL_RUST_ASCENSION "Rust"
#define MEDAL_VOID_ASCENSION "Void"
#define MEDAL_TOOLBOX_SOUL "Toolsoul"
+#define MEDAL_CHEM_TUT "Beginner Chemist"
//Skill medal hub IDs
#define MEDAL_LEGENDARY_MINER "Legendary Miner"
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index e7b7d485493a5f..c5dad7e1334725 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -208,6 +208,8 @@
#define COMSIG_REAGENTS_TEMP_CHANGE "reagents_temp_change"
///from base of [/datum/reagents/proc/handle_reactions]: (num_reactions)
#define COMSIG_REAGENTS_REACTED "reagents_reacted"
+///from base of [/datum/reagents/proc/process]: (num_reactions)
+#define COMSIG_REAGENTS_REACTION_STEP "reagents_time_step"
///from base of [/atom/proc/expose_reagents]: (/atom, /list, methods, volume_modifier, show_message)
#define COMSIG_REAGENTS_EXPOSE_ATOM "reagents_expose_atom"
///from base of [/obj/proc/expose_reagents]: (/obj, /list, methods, volume_modifier, show_message)
diff --git a/code/__DEFINES/reagents.dm b/code/__DEFINES/reagents.dm
index c149c503a69254..e4baf7f9d577da 100644
--- a/code/__DEFINES/reagents.dm
+++ b/code/__DEFINES/reagents.dm
@@ -12,6 +12,7 @@
#define TRANSPARENT (1<<5) // Used on containers which you want to be able to see the reagents off.
#define AMOUNT_VISIBLE (1<<6) // For non-transparent containers that still have the general amount of reagents in them visible.
#define NO_REACT (1<<7) // Applied to a reagent holder, the contents will not react with each other.
+#define REAGENT_HOLDER_INSTANT_REACT (1<<8) // Applied to a reagent holder, all of the reactions in the reagents datum will be instant. Meant to be used for things like smoke effects where reactions aren't meant to occur
// Is an open container for all intents and purposes.
#define OPENCONTAINER (REFILLABLE | DRAINABLE | TRANSPARENT)
@@ -43,3 +44,42 @@
#define CONDIMASTER_STYLE_FALLBACK "_"
#define ALLERGIC_REMOVAL_SKIP "Allergy"
+
+//Used in holder.dm/equlibrium.dm to set values and volume limits
+///stops floating point errors causing issues with checking reagent amounts
+#define CHEMICAL_QUANTISATION_LEVEL 0.0001
+///The smallest amount of volume allowed - prevents tiny numbers
+#define CHEMICAL_VOLUME_MINIMUM 0.001
+///Round to this, to prevent extreme decimal magic and to keep reagent volumes in line with perceived values.
+#define CHEMICAL_VOLUME_ROUNDING 0.01
+///Default pH for reagents datum
+#define CHEMICAL_NORMAL_PH 7.000
+
+//reagent bitflags, used for altering how they works
+///allows on_mob_dead() if present in a dead body
+#define REAGENT_DEAD_PROCESS (1<<0)
+///Do not split the chem at all during processing - ignores all purity effects
+#define REAGENT_DONOTSPLIT (1<<1)
+///Doesn't appear on handheld health analyzers.
+#define REAGENT_INVISIBLE (1<<2)
+///When inverted, the inverted chem uses the name of the original chem
+#define REAGENT_SNEAKYNAME (1<<3)
+///Retains initial volume of chem when splitting for purity effects
+#define REAGENT_SPLITRETAINVOL (1<<4)
+
+//Chemical reaction flags, for determining reaction specialties
+///Convert into impure/pure on reaction completion
+#define REACTION_CLEAR_IMPURE (1<<0)
+///Convert into inverse on reaction completion when purity is low enough
+#define REACTION_CLEAR_INVERSE (1<<1)
+///Clear converted chems retain their purities/inverted purities. Requires 1 or both of the above.
+#define REACTION_CLEAR_RETAIN (1<<2)
+///Used to create instant reactions
+#define REACTION_INSTANT (1<<3)
+///Used to force reactions to create a specific amount of heat per 1u created. So if thermic_constant = 5, for 1u of reagent produced, the heat will be forced up arbitarily by 5 irresepective of other reagents. If you use this, keep in mind standard thermic_constant values are 100x what it should be with this enabled.
+#define REACTION_HEAT_ARBITARY (1<<4)
+///Used to bypass the chem_master transfer block (This is needed for competitive reactions unless you have an end state programmed). More stuff might be added later. When defining this, please add in the comments the associated reactions that it competes with
+#define REACTION_COMPETITIVE (1<<5)
+
+///Used to force an equlibrium to end a reaction in reaction_step() (i.e. in a reaction_step() proc return END_REACTION to end it)
+#define END_REACTION "end_reaction"
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index b6d5cceac545a2..3bdfedbc96b732 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -119,6 +119,7 @@
#define INIT_ORDER_QUIRKS 60
#define INIT_ORDER_TICKER 55
#define INIT_ORDER_TCG 55
+#define INIT_ORDER_REAGENTS 55 //HAS to be before mapping - mapping creates objects, which creates reagents, which relies on lists made in this subsystem
#define INIT_ORDER_MAPPING 50
#define INIT_ORDER_TIMETRACK 47
#define INIT_ORDER_NETWORKS 45
@@ -161,6 +162,7 @@
#define FIRE_PRIORITY_NPC 20
#define FIRE_PRIORITY_PROCESS 25
#define FIRE_PRIORITY_THROWING 25
+#define FIRE_PRIORITY_REAGENTS 26
#define FIRE_PRIORITY_SPACEDRIFT 30
#define FIRE_PRIORITY_FIELDS 30
#define FIRE_PRIOTITY_SMOOTHING 35
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index 08ca05396e8caa..a6f64518d82592 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -11,7 +11,8 @@
#define LAZYINITLIST(L) if (!L) L = list()
#define UNSETEMPTY(L) if (L && !length(L)) L = null
-#define LAZYCOPY(L) (L ? L.Copy() : list() )
+///Like LAZYCOPY - copies an input list if the list has entries, If it doesn't the assigned list is nulled
+#define LAZYLISTDUPLICATE(L) (L ? L.Copy() : null )
#define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } }
#define LAZYADD(L, I) if(!L) { L = list(); } L += I;
#define LAZYOR(L, I) if(!L) { L = list(); } L |= I;
@@ -19,12 +20,17 @@
#define LAZYACCESS(L, I) (L ? (isnum(I) ? (I > 0 && I <= length(L) ? L[I] : null) : L[I]) : null)
#define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V;
#define LAZYLEN(L) length(L)
-#define LAZYCLEARLIST(L) if(L) L.Cut()
-#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
-#define reverseList(L) reverseRange(L.Copy())
+///Sets a list to null
+#define LAZYNULL(L) L = null
#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += list(V);
#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; }
#define LAZYACCESSASSOC(L, I, K) L ? L[I] ? L[I][K] ? L[I][K] : null : null : null
+#define QDEL_LAZYLIST(L) for(var/I in L) qdel(I); L = null;
+//These methods don't null the list
+#define LAZYCOPY(L) (L ? L.Copy() : list() ) //Use LAZYLISTDUPLICATE instead if you want it to null with no entries
+#define LAZYCLEARLIST(L) if(L) L.Cut() // Consider LAZYNULL instead
+#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
+#define reverseList(L) reverseRange(L.Copy())
/// Passed into BINARY_INSERT to compare keys
#define COMPARE_KEY __BIN_LIST[__BIN_MID]
diff --git a/code/__HELPERS/reagents.dm b/code/__HELPERS/reagents.dm
index e96a5509f66668..92c7b872bb1db3 100644
--- a/code/__HELPERS/reagents.dm
+++ b/code/__HELPERS/reagents.dm
@@ -1,4 +1,8 @@
/proc/chem_recipes_do_conflict(datum/chemical_reaction/r1, datum/chemical_reaction/r2)
+ //We have to check to see if either is competitive so can ignore it (competitive reagents are supposed to conflict)
+ if((r1.reaction_flags & REACTION_COMPETITIVE) || (r2.reaction_flags & REACTION_COMPETITIVE))
+ return FALSE
+
//do the non-list tests first, because they are cheaper
if(r1.required_container != r2.required_container)
return FALSE
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 8ff0ac753a446b..e4ac1b2b099917 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -15,6 +15,13 @@
//#define REFERENCE_TRACKING
#ifdef REFERENCE_TRACKING
+/*
+* Enables debug messages for every single reaction step. This is 1 message per 0.5s for a SINGLE reaction. Useful for tracking down bugs/asking me for help in the main reaction handiler (equilibrium.dm).
+*
+* * Requires TESTING to be defined to work.
+*/
+//#define REAGENTS_TESTING
+
///Run a lookup on things hard deleting by default.
//#define GC_FAILURE_HARD_LOOKUP
#ifdef GC_FAILURE_HARD_LOOKUP
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index b9156b8ba1a006..ab3f28a557f965 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -294,3 +294,18 @@ DEFINE_BITFIELD(zap_flags, list(
"ZAP_OBJ_DAMAGE" = ZAP_OBJ_DAMAGE,
"ZAP_GENERATES_POWER" = ZAP_GENERATES_POWER,
))
+
+DEFINE_BITFIELD(chemical_flags, list(
+ "REAGENT_DEAD_PROCESS" = REAGENT_DEAD_PROCESS,
+ "REAGENT_DONOTSPLIT" = REAGENT_DONOTSPLIT,
+ "REAGENT_INVISIBLE" = REAGENT_INVISIBLE,
+ "REAGENT_SNEAKYNAME" = REAGENT_SNEAKYNAME,
+ "REAGENT_SPLITRETAINVOL" = REAGENT_SPLITRETAINVOL,
+))
+
+DEFINE_BITFIELD(reaction_flags, list(
+ "REACTION_CLEAR_IMPURE" = REACTION_CLEAR_IMPURE,
+ "REACTION_CLEAR_INVERSE" = REACTION_CLEAR_INVERSE,
+ "REACTION_CLEAR_RETAIN" = REACTION_CLEAR_RETAIN,
+ "REACTION_INSTANT" = REACTION_INSTANT,
+))
diff --git a/code/controllers/subsystem/processing/reagents.dm b/code/controllers/subsystem/processing/reagents.dm
new file mode 100644
index 00000000000000..344c066adf2802
--- /dev/null
+++ b/code/controllers/subsystem/processing/reagents.dm
@@ -0,0 +1,42 @@
+//Used for active reactions in reagents/equilibrium datums
+
+PROCESSING_SUBSYSTEM_DEF(reagents)
+ name = "Reagents"
+ init_order = INIT_ORDER_REAGENTS
+ priority = FIRE_PRIORITY_REAGENTS
+ wait = 0.25 SECONDS //You might think that rate_up_lim has to be set to half, but since everything is normalised around delta_time, it automatically adjusts it to be per second. Magic!
+ flags = SS_KEEP_TIMING
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ ///What time was it when we last ticked
+ var/previous_world_time = 0
+
+/datum/controller/subsystem/processing/reagents/Initialize()
+ . = ..()
+ //So our first step isn't insane
+ previous_world_time = world.time
+ //Build GLOB lists - see holder.dm
+ build_chemical_reagent_list()
+ build_chemical_reactions_list()
+ return
+
+/datum/controller/subsystem/processing/reagents/fire(resumed = FALSE)
+ if (!resumed)
+ currentrun = processing.Copy()
+ //cache for sanic speed (lists are references anyways)
+ var/list/current_run = currentrun
+
+ //Attempt to realtime reactions in a way that doesn't make them overtly dangerous
+ var/delta_realtime = (world.time - previous_world_time)/10 //normalise to s from ds
+ previous_world_time = world.time
+
+ while(current_run.len)
+ var/datum/thing = current_run[current_run.len]
+ current_run.len--
+ if(QDELETED(thing))
+ stack_trace("Found qdeleted thing in [type], in the current_run list.")
+ processing -= thing
+ else if(thing.process(delta_realtime) == PROCESS_KILL) //we are realtime
+ // fully stop so that a future START_PROCESSING will work
+ STOP_PROCESSING(src, thing)
+ if (MC_TICK_CHECK)
+ return
diff --git a/code/datums/achievements/misc_achievements.dm b/code/datums/achievements/misc_achievements.dm
index c08b2d9dbbdd46..0da38df8f306a3 100644
--- a/code/datums/achievements/misc_achievements.dm
+++ b/code/datums/achievements/misc_achievements.dm
@@ -153,3 +153,9 @@
desc = "My eternal soul was destroyed to make a toolbox look funny and all I got was this achievement..."
database_id = MEDAL_TOOLBOX_SOUL
icon = "toolbox_soul"
+
+/datum/award/achievement/misc/chemistry_tut
+ name = "Perfect chemistry blossom"
+ desc = "Passed the chemistry tutorial with perfect purity!"
+ database_id = MEDAL_CHEM_TUT
+ icon = "chem_tut"
diff --git a/code/datums/components/plumbing/reaction_chamber.dm b/code/datums/components/plumbing/reaction_chamber.dm
index 88d7c760a7dc09..07ee12b108edd7 100644
--- a/code/datums/components/plumbing/reaction_chamber.dm
+++ b/code/datums/components/plumbing/reaction_chamber.dm
@@ -36,3 +36,9 @@
RC.emptying = TRUE //If we move this up, it'll instantly get turned off since any reaction always sets the reagent_total to zero. Other option is make the reaction update
//everything for every chemical removed, wich isn't a good option either.
RC.on_reagent_change(reagents) //We need to check it now, because some reactions leave nothing left.
+
+
+/datum/component/plumbing/reaction_chamber/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net)
+ if(reagents.is_reacting == TRUE) //Let the thing react in peace
+ return
+ return ..()
diff --git a/code/datums/quirks/negative.dm b/code/datums/quirks/negative.dm
index 3421b5f550c5b6..2ec0d00f540d86 100644
--- a/code/datums/quirks/negative.dm
+++ b/code/datums/quirks/negative.dm
@@ -193,7 +193,7 @@
if("Psychologist")
heirloom_type = /obj/item/storage/pill_bottle
if("Chemist")
- heirloom_type = /obj/item/book/manual/wiki/chemistry
+ heirloom_type = pick(/obj/item/book/manual/wiki/chemistry, /obj/item/ph_booklet)
if("Virologist")
heirloom_type = /obj/item/reagent_containers/syringe
//Engineering
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 630c68415da44c..4af731ba37cde1 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -611,7 +611,10 @@
if(length(reagents.reagent_list))
if(user.can_see_reagents()) //Show each individual reagent
for(var/datum/reagent/R in reagents.reagent_list)
- . += "[R.volume] units of [R.name]"
+ . += "[round(R.volume, 0.01)] units of [R.name]"
+ if(reagents.is_reacting)
+ . += "It is currently reacting!"
+ . += "The solution's pH is [round(reagents.ph, 0.01)] and has a temperature of [reagents.chem_temp]K."
else //Otherwise, just show the total volume
var/total_volume = 0
for(var/datum/reagent/R in reagents.reagent_list)
diff --git a/code/game/machinery/Sleeper.dm b/code/game/machinery/Sleeper.dm
index 4f36f7ac73efbf..cca2561c4f7305 100644
--- a/code/game/machinery/Sleeper.dm
+++ b/code/game/machinery/Sleeper.dm
@@ -202,6 +202,8 @@
data["occupant"]["reagents"] = list()
if(mob_occupant.reagents && mob_occupant.reagents.reagent_list.len)
for(var/datum/reagent/R in mob_occupant.reagents.reagent_list)
+ if(R.chemical_flags & REAGENT_INVISIBLE) //Don't show hidden chems
+ continue
data["occupant"]["reagents"] += list(list("name" = R.name, "volume" = R.volume))
return data
diff --git a/code/game/machinery/medical_kiosk.dm b/code/game/machinery/medical_kiosk.dm
index 5f1c2af708b913..3cd0b413d7c735 100644
--- a/code/game/machinery/medical_kiosk.dm
+++ b/code/game/machinery/medical_kiosk.dm
@@ -235,6 +235,8 @@
if(altPatient.reagents.reagent_list.len) //Chemical Analysis details.
for(var/r in altPatient.reagents.reagent_list)
var/datum/reagent/reagent = r
+ if(reagent.chemical_flags & REAGENT_INVISIBLE) //Don't show hidden chems
+ continue
chemical_list += list(list("name" = reagent.name, "volume" = round(reagent.volume, 0.01)))
if(reagent.overdosed)
overdose_list += list(list("name" = reagent.name))
@@ -242,6 +244,8 @@
if(belly?.reagents.reagent_list.len) //include the stomach contents if it exists
for(var/bile in belly.reagents.reagent_list)
var/datum/reagent/bit = bile
+ if(bit.chemical_flags & REAGENT_INVISIBLE) //Don't show hidden chems
+ continue
if(!belly.food_reagents[bit.type])
chemical_list += list(list("name" = bit.name, "volume" = round(bit.volume, 0.01)))
else
diff --git a/code/game/objects/effects/effect_system/effects_foam.dm b/code/game/objects/effects/effect_system/effects_foam.dm
index 0466504ac5cc2d..513fc71153a80f 100644
--- a/code/game/objects/effects/effect_system/effects_foam.dm
+++ b/code/game/objects/effects/effect_system/effects_foam.dm
@@ -91,7 +91,7 @@
/obj/effect/particle_effect/foam/Initialize()
. = ..()
- create_reagents(1000) //limited by the size of the reagent holder anyway.
+ create_reagents(1000, REAGENT_HOLDER_INSTANT_REACT) //limited by the size of the reagent holder anyway. Works without instant possibly edit in future
START_PROCESSING(SSfastprocess, src)
playsound(src, 'sound/effects/bubbles2.ogg', 80, TRUE, -3)
@@ -224,7 +224,7 @@
/datum/effect_system/foam_spread/New()
..()
chemholder = new /obj()
- var/datum/reagents/R = new/datum/reagents(1000)
+ var/datum/reagents/R = new/datum/reagents(1000, REAGENT_HOLDER_INSTANT_REACT) //same as above
chemholder.reagents = R
R.my_atom = chemholder
diff --git a/code/game/objects/effects/effect_system/effects_smoke.dm b/code/game/objects/effects/effect_system/effects_smoke.dm
index 04a4f901676814..aaf4f52889bc13 100644
--- a/code/game/objects/effects/effect_system/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/effects_smoke.dm
@@ -263,8 +263,9 @@
/datum/effect_system/smoke_spread/chem/New()
..()
chemholder = new /obj()
- var/datum/reagents/R = new/datum/reagents(500)
+ var/datum/reagents/R = new (500, REAGENT_HOLDER_INSTANT_REACT) //This is a safety for now to prevent smoke generating more smoke as the smoke reagents react in the smoke. This is prevented naturally from happening even if this is off, but I want to be sure that any edge cases are prevented before I get a chance to rework smoke reactions (specifically adding water or reacting away stabilizing agent in the middle of it).
chemholder.reagents = R
+
R.my_atom = chemholder
/datum/effect_system/smoke_spread/chem/Destroy()
@@ -288,7 +289,7 @@
contained = "\[[contained]\]"
var/where = "[AREACOORD(location)]"
- if(carry.my_atom.fingerprintslast)
+ if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
var/more = ""
if(M)
diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm
index c217af95f943b2..ea2ea76c45f0a8 100644
--- a/code/game/objects/items/devices/scanners.dm
+++ b/code/game/objects/items/devices/scanners.dm
@@ -430,6 +430,8 @@ GENE SCANNER
render_list += "Subject contains the following reagents in their blood:\n"
for(var/r in M.reagents.reagent_list)
var/datum/reagent/reagent = r
+ if(reagent.chemical_flags & REAGENT_INVISIBLE) //Don't show hidden chems on scanners
+ continue
render_list += "[round(reagent.volume, 0.001)] units of [reagent.name][reagent.overdosed ? " - OVERDOSING" : "."]\n"
else
render_list += "Subject contains no reagents in their blood.\n"
@@ -439,6 +441,8 @@ GENE SCANNER
render_list += "Subject contains the following reagents in their stomach:\n"
for(var/bile in belly.reagents.reagent_list)
var/datum/reagent/bit = bile
+ if(bit.chemical_flags & REAGENT_INVISIBLE) //Don't show hidden chems on scanners
+ continue
if(!belly.food_reagents[bit.type])
render_list += "[round(bit.volume, 0.001)] units of [bit.name][bit.overdosed ? " - OVERDOSING" : "."]\n"
else
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/medical.dm b/code/game/objects/structures/crates_lockers/closets/secure/medical.dm
index e5705f28015902..8e01f29c65af0a 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/medical.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/medical.dm
@@ -120,6 +120,9 @@
new /obj/item/storage/box/pillbottles(src)
new /obj/item/storage/box/medigels(src)
new /obj/item/storage/box/medigels(src)
+ new /obj/item/ph_booklet(src)
+ new /obj/item/reagent_containers/dropper(src)
+ new /obj/item/reagent_containers/glass/bottle/acidic_buffer(src) //hopefully they get the hint
/obj/structure/closet/secure_closet/chemical/heisenberg //contains one of each beaker, syringe etc.
name = "advanced chemical closet"
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 62bcf6e02888af..dd2bc8d46324d9 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -524,3 +524,8 @@
/proc/sanitize_css_class_name(name)
var/static/regex/regex = new(@"[^a-zA-Z0-9]","g")
return replacetext(name, regex, "")
+
+/datum/asset/simple/tutorial_advisors
+ assets = list(
+ "chem_help_advisor.gif" = 'icons/UI_Icons/Advisors/chem_help_advisor.gif',
+ )
diff --git a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
index 45978644c63874..d2561fbc86692d 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm
@@ -131,7 +131,8 @@ GLOBAL_LIST_INIT(oilfry_blacklisted_items, typecacheof(list(
reagents.expose(C, TOUCH)
var/permeability = 1 - C.get_permeability_protection(list(HEAD))
C.apply_damage(min(30 * permeability, reagents.total_volume), BURN, BODY_ZONE_HEAD)
- reagents.remove_any((reagents.total_volume/2))
+ if(reagents.reagent_list) //This can runtime if reagents has nothing in it.
+ reagents.remove_any((reagents.total_volume/2))
C.Paralyze(60)
user.changeNext_move(CLICK_CD_MELEE)
return ..()
diff --git a/code/modules/food_and_drinks/recipes/drinks_recipes.dm b/code/modules/food_and_drinks/recipes/drinks_recipes.dm
index c7fc75b956e2fc..da378521c4a247 100644
--- a/code/modules/food_and_drinks/recipes/drinks_recipes.dm
+++ b/code/modules/food_and_drinks/recipes/drinks_recipes.dm
@@ -1,594 +1,610 @@
+//////////////////////////////////////// DRINK RECIPE BASE ////////////////////////////////
+
+/datum/chemical_reaction/drink
+ optimal_temp = 400
+ temp_exponent_factor = 1
+ optimal_ph_min = 2
+ optimal_ph_max = 10
+ thermic_constant = 0
+ H_ion_release = 0
+ rate_up_lim = 50
+
////////////////////////////////////////// COCKTAILS //////////////////////////////////////
-/datum/chemical_reaction/goldschlager
+/datum/chemical_reaction/drink/goldschlager
results = list(/datum/reagent/consumable/ethanol/goldschlager = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 10, /datum/reagent/gold = 1)
-/datum/chemical_reaction/patron
+/datum/chemical_reaction/drink/patron
results = list(/datum/reagent/consumable/ethanol/patron = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/tequila = 10, /datum/reagent/silver = 1)
-/datum/chemical_reaction/bilk
+/datum/chemical_reaction/drink/bilk
results = list(/datum/reagent/consumable/ethanol/bilk = 2)
required_reagents = list(/datum/reagent/consumable/milk = 1, /datum/reagent/consumable/ethanol/beer = 1)
-/datum/chemical_reaction/icetea
+/datum/chemical_reaction/drink/icetea
results = list(/datum/reagent/consumable/icetea = 4)
required_reagents = list(/datum/reagent/consumable/ice = 1, /datum/reagent/consumable/tea = 3)
-/datum/chemical_reaction/icecoffee
+/datum/chemical_reaction/drink/icecoffee
results = list(/datum/reagent/consumable/icecoffee = 4)
required_reagents = list(/datum/reagent/consumable/ice = 1, /datum/reagent/consumable/coffee = 3)
-/datum/chemical_reaction/hoticecoffee
+/datum/chemical_reaction/drink/hoticecoffee
results = list(/datum/reagent/consumable/hot_ice_coffee = 3)
required_reagents = list(/datum/reagent/toxin/hot_ice = 1, /datum/reagent/consumable/coffee = 2)
-/datum/chemical_reaction/nuka_cola
+/datum/chemical_reaction/drink/nuka_cola
results = list(/datum/reagent/consumable/nuka_cola = 6)
required_reagents = list(/datum/reagent/uranium = 1, /datum/reagent/consumable/space_cola = 6)
-/datum/chemical_reaction/moonshine
+/datum/chemical_reaction/drink/moonshine
results = list(/datum/reagent/consumable/ethanol/moonshine = 10)
required_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/consumable/sugar = 5)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/wine
+/datum/chemical_reaction/drink/wine
results = list(/datum/reagent/consumable/ethanol/wine = 10)
required_reagents = list(/datum/reagent/consumable/grapejuice = 10)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/spacebeer
+/datum/chemical_reaction/drink/spacebeer
results = list(/datum/reagent/consumable/ethanol/beer = 10)
required_reagents = list(/datum/reagent/consumable/flour = 10)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/vodka
+/datum/chemical_reaction/drink/vodka
results = list(/datum/reagent/consumable/ethanol/vodka = 10)
required_reagents = list(/datum/reagent/consumable/potato_juice = 10)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/kahlua
+/datum/chemical_reaction/drink/kahlua
results = list(/datum/reagent/consumable/ethanol/kahlua = 5)
required_reagents = list(/datum/reagent/consumable/coffee = 5, /datum/reagent/consumable/sugar = 5)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/gin_tonic
+/datum/chemical_reaction/drink/gin_tonic
results = list(/datum/reagent/consumable/ethanol/gintonic = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/tonic = 1)
-/datum/chemical_reaction/rum_coke
+/datum/chemical_reaction/drink/rum_coke
results = list(/datum/reagent/consumable/ethanol/rum_coke = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 2, /datum/reagent/consumable/space_cola = 1)
-/datum/chemical_reaction/cuba_libre
+/datum/chemical_reaction/drink/cuba_libre
results = list(/datum/reagent/consumable/ethanol/cuba_libre = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/rum_coke = 3, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/martini
+/datum/chemical_reaction/drink/martini
results = list(/datum/reagent/consumable/ethanol/martini = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/ethanol/vermouth = 1)
-/datum/chemical_reaction/vodkamartini
+/datum/chemical_reaction/drink/vodkamartini
results = list(/datum/reagent/consumable/ethanol/vodkamartini = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/ethanol/vermouth = 1)
-/datum/chemical_reaction/white_russian
+/datum/chemical_reaction/drink/white_russian
results = list(/datum/reagent/consumable/ethanol/white_russian = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/black_russian = 3, /datum/reagent/consumable/cream = 2)
-/datum/chemical_reaction/whiskey_cola
+/datum/chemical_reaction/drink/whiskey_cola
results = list(/datum/reagent/consumable/ethanol/whiskey_cola = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 2, /datum/reagent/consumable/space_cola = 1)
-/datum/chemical_reaction/screwdriver
+/datum/chemical_reaction/drink/screwdriver
results = list(/datum/reagent/consumable/ethanol/screwdrivercocktail = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/orangejuice = 1)
-/datum/chemical_reaction/bloody_mary
+/datum/chemical_reaction/drink/bloody_mary
results = list(/datum/reagent/consumable/ethanol/bloody_mary = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/tomatojuice = 2, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/gargle_blaster
+/datum/chemical_reaction/drink/gargle_blaster
results = list(/datum/reagent/consumable/ethanol/gargle_blaster = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/ethanol/gin = 1, /datum/reagent/consumable/ethanol/whiskey = 1, /datum/reagent/consumable/ethanol/cognac = 1, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/brave_bull
+/datum/chemical_reaction/drink/brave_bull
results = list(/datum/reagent/consumable/ethanol/brave_bull = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/tequila = 2, /datum/reagent/consumable/ethanol/kahlua = 1)
-/datum/chemical_reaction/tequila_sunrise
+/datum/chemical_reaction/drink/tequila_sunrise
results = list(/datum/reagent/consumable/ethanol/tequila_sunrise = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/tequila = 2, /datum/reagent/consumable/orangejuice = 2, /datum/reagent/consumable/grenadine = 1)
-/datum/chemical_reaction/toxins_special
+/datum/chemical_reaction/drink/toxins_special
results = list(/datum/reagent/consumable/ethanol/toxins_special = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 2, /datum/reagent/consumable/ethanol/vermouth = 1, /datum/reagent/toxin/plasma = 2)
-/datum/chemical_reaction/beepsky_smash
+/datum/chemical_reaction/drink/beepsky_smash
results = list(/datum/reagent/consumable/ethanol/beepsky_smash = 5)
required_reagents = list(/datum/reagent/consumable/limejuice = 2, /datum/reagent/consumable/ethanol/quadruple_sec = 2, /datum/reagent/iron = 1)
-/datum/chemical_reaction/doctor_delight
+/datum/chemical_reaction/drink/doctor_delight
results = list(/datum/reagent/consumable/doctor_delight = 5)
required_reagents = list(/datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/tomatojuice = 1, /datum/reagent/consumable/orangejuice = 1, /datum/reagent/consumable/cream = 1, /datum/reagent/medicine/cryoxadone = 1)
-/datum/chemical_reaction/irish_cream
+/datum/chemical_reaction/drink/irish_cream
results = list(/datum/reagent/consumable/ethanol/irish_cream = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 2, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/manly_dorf
+/datum/chemical_reaction/drink/manly_dorf
results = list(/datum/reagent/consumable/ethanol/manly_dorf = 3)
required_reagents = list (/datum/reagent/consumable/ethanol/beer = 1, /datum/reagent/consumable/ethanol/ale = 2)
-/datum/chemical_reaction/greenbeer
+/datum/chemical_reaction/drink/greenbeer
results = list(/datum/reagent/consumable/ethanol/beer/green = 10)
required_reagents = list(/datum/reagent/colorful_reagent/powder/green = 1, /datum/reagent/consumable/ethanol/beer = 10)
-/datum/chemical_reaction/greenbeer2 //apparently there's no other way to do this
+/datum/chemical_reaction/drink/greenbeer2 //apparently there's no other way to do this
results = list(/datum/reagent/consumable/ethanol/beer/green = 10)
required_reagents = list(/datum/reagent/colorful_reagent/powder/green/crayon = 1, /datum/reagent/consumable/ethanol/beer = 10)
-/datum/chemical_reaction/hooch
+/datum/chemical_reaction/drink/hooch
results = list(/datum/reagent/consumable/ethanol/hooch = 3)
required_reagents = list (/datum/reagent/consumable/ethanol = 2, /datum/reagent/fuel = 1)
required_catalysts = list(/datum/reagent/consumable/enzyme = 1)
-/datum/chemical_reaction/irish_coffee
+/datum/chemical_reaction/drink/irish_coffee
results = list(/datum/reagent/consumable/ethanol/irishcoffee = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/irish_cream = 1, /datum/reagent/consumable/coffee = 1)
-/datum/chemical_reaction/b52
+/datum/chemical_reaction/drink/b52
results = list(/datum/reagent/consumable/ethanol/b52 = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/irish_cream = 1, /datum/reagent/consumable/ethanol/kahlua = 1, /datum/reagent/consumable/ethanol/cognac = 1)
-/datum/chemical_reaction/atomicbomb
+/datum/chemical_reaction/drink/atomicbomb
results = list(/datum/reagent/consumable/ethanol/atomicbomb = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/b52 = 10, /datum/reagent/uranium = 1)
-/datum/chemical_reaction/margarita
+/datum/chemical_reaction/drink/margarita
results = list(/datum/reagent/consumable/ethanol/margarita = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/tequila = 2, /datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/ethanol/triple_sec = 1)
-/datum/chemical_reaction/longislandicedtea
+/datum/chemical_reaction/drink/longislandicedtea
results = list(/datum/reagent/consumable/ethanol/longislandicedtea = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/ethanol/gin = 1, /datum/reagent/consumable/ethanol/tequila = 1, /datum/reagent/consumable/ethanol/cuba_libre = 1)
-/datum/chemical_reaction/threemileisland
+/datum/chemical_reaction/drink/threemileisland
results = list(/datum/reagent/consumable/ethanol/threemileisland = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/longislandicedtea = 10, /datum/reagent/uranium = 1)
-/datum/chemical_reaction/whiskeysoda
+/datum/chemical_reaction/drink/whiskeysoda
results = list(/datum/reagent/consumable/ethanol/whiskeysoda = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 2, /datum/reagent/consumable/sodawater = 1)
-/datum/chemical_reaction/black_russian
+/datum/chemical_reaction/drink/black_russian
results = list(/datum/reagent/consumable/ethanol/black_russian = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 3, /datum/reagent/consumable/ethanol/kahlua = 2)
-/datum/chemical_reaction/hiveminderaser
+/datum/chemical_reaction/drink/hiveminderaser
results = list(/datum/reagent/consumable/ethanol/hiveminderaser = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/black_russian = 2, /datum/reagent/consumable/ethanol/thirteenloko = 1, /datum/reagent/consumable/grenadine = 1)
-/datum/chemical_reaction/manhattan
+/datum/chemical_reaction/drink/manhattan
results = list(/datum/reagent/consumable/ethanol/manhattan = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 2, /datum/reagent/consumable/ethanol/vermouth = 1)
-/datum/chemical_reaction/manhattan_proj
+/datum/chemical_reaction/drink/manhattan_proj
results = list(/datum/reagent/consumable/ethanol/manhattan_proj = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/manhattan = 10, /datum/reagent/uranium = 1)
-/datum/chemical_reaction/vodka_tonic
+/datum/chemical_reaction/drink/vodka_tonic
results = list(/datum/reagent/consumable/ethanol/vodkatonic = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/tonic = 1)
-/datum/chemical_reaction/gin_fizz
+/datum/chemical_reaction/drink/gin_fizz
results = list(/datum/reagent/consumable/ethanol/ginfizz = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/sodawater = 1, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/bahama_mama
+/datum/chemical_reaction/drink/bahama_mama
results = list(/datum/reagent/consumable/ethanol/bahama_mama = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/creme_de_coconut = 1, /datum/reagent/consumable/ethanol/kahlua = 1, /datum/reagent/consumable/ethanol/rum = 2, /datum/reagent/consumable/pineapplejuice = 1)
-/datum/chemical_reaction/singulo
+/datum/chemical_reaction/drink/singulo
results = list(/datum/reagent/consumable/ethanol/singulo = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 5, /datum/reagent/uranium/radium = 1, /datum/reagent/consumable/ethanol/wine = 5)
-/datum/chemical_reaction/alliescocktail
+/datum/chemical_reaction/drink/alliescocktail
results = list(/datum/reagent/consumable/ethanol/alliescocktail = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/martini = 1, /datum/reagent/consumable/ethanol/vodka = 1)
-/datum/chemical_reaction/demonsblood
+/datum/chemical_reaction/drink/demonsblood
results = list(/datum/reagent/consumable/ethanol/demonsblood = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/spacemountainwind = 1, /datum/reagent/blood = 1, /datum/reagent/consumable/dr_gibb = 1)
-/datum/chemical_reaction/booger
+/datum/chemical_reaction/drink/booger
results = list(/datum/reagent/consumable/ethanol/booger = 4)
required_reagents = list(/datum/reagent/consumable/cream = 1, /datum/reagent/consumable/banana = 1, /datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/watermelonjuice = 1)
-/datum/chemical_reaction/antifreeze
+/datum/chemical_reaction/drink/antifreeze
results = list(/datum/reagent/consumable/ethanol/antifreeze = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 2, /datum/reagent/consumable/cream = 1, /datum/reagent/consumable/ice = 1)
-/datum/chemical_reaction/barefoot
+/datum/chemical_reaction/drink/barefoot
results = list(/datum/reagent/consumable/ethanol/barefoot = 3)
required_reagents = list(/datum/reagent/consumable/berryjuice = 1, /datum/reagent/consumable/cream = 1, /datum/reagent/consumable/ethanol/vermouth = 1)
-/datum/chemical_reaction/moscow_mule
+/datum/chemical_reaction/drink/moscow_mule
results = list(/datum/reagent/consumable/ethanol/moscow_mule = 10)
required_reagents = list(/datum/reagent/consumable/sol_dry = 5, /datum/reagent/consumable/ethanol/vodka = 5, /datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/ice = 1)
mix_sound = 'sound/effects/bubbles2.ogg'
-/datum/chemical_reaction/painkiller
+/datum/chemical_reaction/drink/painkiller
results = list(/datum/reagent/consumable/ethanol/painkiller = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/creme_de_coconut = 5, /datum/reagent/consumable/pineapplejuice = 4, /datum/reagent/consumable/orangejuice = 1)
-/datum/chemical_reaction/pina_colada
+/datum/chemical_reaction/drink/pina_colada
results = list(/datum/reagent/consumable/ethanol/pina_colada = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/creme_de_coconut = 1, /datum/reagent/consumable/pineapplejuice = 3, /datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/limejuice = 1)
////DRINKS THAT REQUIRED IMPROVED SPRITES BELOW:: -Agouri/////
-/datum/chemical_reaction/sbiten
+/datum/chemical_reaction/drink/sbiten
results = list(/datum/reagent/consumable/ethanol/sbiten = 10)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 10, /datum/reagent/consumable/capsaicin = 1)
-/datum/chemical_reaction/red_mead
+/datum/chemical_reaction/drink/red_mead
results = list(/datum/reagent/consumable/ethanol/red_mead = 2)
required_reagents = list(/datum/reagent/blood = 1, /datum/reagent/consumable/ethanol/mead = 1)
-/datum/chemical_reaction/mead
+/datum/chemical_reaction/drink/mead
results = list(/datum/reagent/consumable/ethanol/mead = 2)
required_reagents = list(/datum/reagent/consumable/honey = 2)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
-/datum/chemical_reaction/iced_beer
+/datum/chemical_reaction/drink/iced_beer
results = list(/datum/reagent/consumable/ethanol/iced_beer = 6)
required_reagents = list(/datum/reagent/consumable/ethanol/beer = 5, /datum/reagent/consumable/ice = 1)
-/datum/chemical_reaction/grog
+/datum/chemical_reaction/drink/grog
results = list(/datum/reagent/consumable/ethanol/grog = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/water = 1)
-/datum/chemical_reaction/soy_latte
+/datum/chemical_reaction/drink/soy_latte
results = list(/datum/reagent/consumable/soy_latte = 2)
required_reagents = list(/datum/reagent/consumable/coffee = 1, /datum/reagent/consumable/soymilk = 1)
-/datum/chemical_reaction/cafe_latte
+/datum/chemical_reaction/drink/cafe_latte
results = list(/datum/reagent/consumable/cafe_latte = 2)
required_reagents = list(/datum/reagent/consumable/coffee = 1, /datum/reagent/consumable/milk = 1)
-/datum/chemical_reaction/acidspit
+/datum/chemical_reaction/drink/acidspit
results = list(/datum/reagent/consumable/ethanol/acid_spit = 6)
required_reagents = list(/datum/reagent/toxin/acid = 1, /datum/reagent/consumable/ethanol/wine = 5)
+ optimal_ph_min = 0 //Our reaction is very acidic, so lets shift our range
-/datum/chemical_reaction/amasec
+/datum/chemical_reaction/drink/amasec
results = list(/datum/reagent/consumable/ethanol/amasec = 10)
required_reagents = list(/datum/reagent/iron = 1, /datum/reagent/consumable/ethanol/wine = 5, /datum/reagent/consumable/ethanol/vodka = 5)
-/datum/chemical_reaction/changelingsting
+/datum/chemical_reaction/drink/changelingsting
results = list(/datum/reagent/consumable/ethanol/changelingsting = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/screwdrivercocktail = 1, /datum/reagent/consumable/lemon_lime = 2)
-/datum/chemical_reaction/aloe
+/datum/chemical_reaction/drink/aloe
results = list(/datum/reagent/consumable/ethanol/aloe = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/irish_cream = 1, /datum/reagent/consumable/watermelonjuice = 1)
-/datum/chemical_reaction/andalusia
+/datum/chemical_reaction/drink/andalusia
results = list(/datum/reagent/consumable/ethanol/andalusia = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/ethanol/whiskey = 1, /datum/reagent/consumable/lemonjuice = 1)
-/datum/chemical_reaction/neurotoxin
+/datum/chemical_reaction/drink/neurotoxin
results = list(/datum/reagent/consumable/ethanol/neurotoxin = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/gargle_blaster = 1, /datum/reagent/medicine/morphine = 1)
-/datum/chemical_reaction/snowwhite
+/datum/chemical_reaction/drink/snowwhite
results = list(/datum/reagent/consumable/ethanol/snowwhite = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/beer = 1, /datum/reagent/consumable/lemon_lime = 1)
-/datum/chemical_reaction/irishcarbomb
+/datum/chemical_reaction/drink/irishcarbomb
results = list(/datum/reagent/consumable/ethanol/irishcarbomb = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/ale = 1, /datum/reagent/consumable/ethanol/irish_cream = 1)
-/datum/chemical_reaction/syndicatebomb
+/datum/chemical_reaction/drink/syndicatebomb
results = list(/datum/reagent/consumable/ethanol/syndicatebomb = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/beer = 1, /datum/reagent/consumable/ethanol/whiskey_cola = 1)
-/datum/chemical_reaction/erikasurprise
+/datum/chemical_reaction/drink/erikasurprise
results = list(/datum/reagent/consumable/ethanol/erikasurprise = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/ale = 1, /datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/ethanol/whiskey = 1, /datum/reagent/consumable/banana = 1, /datum/reagent/consumable/ice = 1)
-/datum/chemical_reaction/devilskiss
+/datum/chemical_reaction/drink/devilskiss
results = list(/datum/reagent/consumable/ethanol/devilskiss = 3)
required_reagents = list(/datum/reagent/blood = 1, /datum/reagent/consumable/ethanol/kahlua = 1, /datum/reagent/consumable/ethanol/rum = 1)
-/datum/chemical_reaction/hippiesdelight
+/datum/chemical_reaction/drink/hippiesdelight
results = list(/datum/reagent/consumable/ethanol/hippies_delight = 2)
required_reagents = list(/datum/reagent/drug/mushroomhallucinogen = 1, /datum/reagent/consumable/ethanol/gargle_blaster = 1)
-/datum/chemical_reaction/bananahonk
+/datum/chemical_reaction/drink/bananahonk
results = list(/datum/reagent/consumable/ethanol/bananahonk = 2)
required_reagents = list(/datum/reagent/consumable/laughter = 1, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/silencer
+/datum/chemical_reaction/drink/silencer
results = list(/datum/reagent/consumable/ethanol/silencer = 3)
required_reagents = list(/datum/reagent/consumable/nothing = 1, /datum/reagent/consumable/cream = 1, /datum/reagent/consumable/sugar = 1)
-/datum/chemical_reaction/driestmartini
+/datum/chemical_reaction/drink/driestmartini
results = list(/datum/reagent/consumable/ethanol/driestmartini = 2)
required_reagents = list(/datum/reagent/consumable/nothing = 1, /datum/reagent/consumable/ethanol/gin = 1)
-/datum/chemical_reaction/thirteenloko
+/datum/chemical_reaction/drink/thirteenloko
results = list(/datum/reagent/consumable/ethanol/thirteenloko = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/coffee = 1, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/cherryshake
+/datum/chemical_reaction/drink/cherryshake
results = list(/datum/reagent/consumable/cherryshake = 3)
required_reagents = list(/datum/reagent/consumable/cherryjelly = 1, /datum/reagent/consumable/ice = 1, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/bluecherryshake
+/datum/chemical_reaction/drink/bluecherryshake
results = list(/datum/reagent/consumable/bluecherryshake = 3)
required_reagents = list(/datum/reagent/consumable/bluecherryjelly = 1, /datum/reagent/consumable/ice = 1, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/drunkenblumpkin
+/datum/chemical_reaction/drink/drunkenblumpkin
results = list(/datum/reagent/consumable/ethanol/drunkenblumpkin = 4)
required_reagents = list(/datum/reagent/consumable/blumpkinjuice = 1, /datum/reagent/consumable/ethanol/irish_cream = 2, /datum/reagent/consumable/ice = 1)
-/datum/chemical_reaction/pumpkin_latte
+/datum/chemical_reaction/drink/pumpkin_latte
results = list(/datum/reagent/consumable/pumpkin_latte = 15)
required_reagents = list(/datum/reagent/consumable/pumpkinjuice = 5, /datum/reagent/consumable/coffee = 5, /datum/reagent/consumable/cream = 5)
-/datum/chemical_reaction/gibbfloats
+/datum/chemical_reaction/drink/gibbfloats
results = list(/datum/reagent/consumable/gibbfloats = 15)
required_reagents = list(/datum/reagent/consumable/dr_gibb = 5, /datum/reagent/consumable/ice = 5, /datum/reagent/consumable/cream = 5)
-/datum/chemical_reaction/triple_citrus
+/datum/chemical_reaction/drink/triple_citrus
results = list(/datum/reagent/consumable/triple_citrus = 5)
required_reagents = list(/datum/reagent/consumable/lemonjuice = 1, /datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/orangejuice = 1)
+ optimal_ph_min = 0//Our reaction is very acidic, so lets shift our range
-/datum/chemical_reaction/grape_soda
+/datum/chemical_reaction/drink/grape_soda
results = list(/datum/reagent/consumable/grape_soda = 2)
required_reagents = list(/datum/reagent/consumable/grapejuice = 1, /datum/reagent/consumable/sodawater = 1)
-/datum/chemical_reaction/grappa
+/datum/chemical_reaction/drink/grappa
results = list(/datum/reagent/consumable/ethanol/grappa = 10)
required_reagents = list (/datum/reagent/consumable/ethanol/wine = 10)
required_catalysts = list (/datum/reagent/consumable/enzyme = 10)
-/datum/chemical_reaction/whiskey_sour
+/datum/chemical_reaction/drink/whiskey_sour
results = list(/datum/reagent/consumable/ethanol/whiskey_sour = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskey = 1, /datum/reagent/consumable/lemonjuice = 1, /datum/reagent/consumable/sugar = 1)
mix_message = "The mixture darkens to a rich gold hue."
-/datum/chemical_reaction/fetching_fizz
+/datum/chemical_reaction/drink/fetching_fizz
results = list(/datum/reagent/consumable/ethanol/fetching_fizz = 3)
required_reagents = list(/datum/reagent/consumable/nuka_cola = 1, /datum/reagent/iron = 1) //Manufacturable from only the mining station
mix_message = "The mixture slightly vibrates before settling."
-/datum/chemical_reaction/hearty_punch
+/datum/chemical_reaction/drink/hearty_punch
results = list(/datum/reagent/consumable/ethanol/hearty_punch = 1) //Very little, for balance reasons
required_reagents = list(/datum/reagent/consumable/ethanol/brave_bull = 5, /datum/reagent/consumable/ethanol/syndicatebomb = 5, /datum/reagent/consumable/ethanol/absinthe = 5)
mix_message = "The mixture darkens to a healthy crimson."
required_temp = 315 //Piping hot!
-/datum/chemical_reaction/bacchus_blessing
+/datum/chemical_reaction/drink/bacchus_blessing
results = list(/datum/reagent/consumable/ethanol/bacchus_blessing = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/hooch = 1, /datum/reagent/consumable/ethanol/absinthe = 1, /datum/reagent/consumable/ethanol/manly_dorf = 1, /datum/reagent/consumable/ethanol/syndicatebomb = 1)
mix_message = "The mixture turns to a sickening froth."
-/datum/chemical_reaction/lemonade
+/datum/chemical_reaction/drink/lemonade
results = list(/datum/reagent/consumable/lemonade = 5)
required_reagents = list(/datum/reagent/consumable/lemonjuice = 2, /datum/reagent/water = 2, /datum/reagent/consumable/sugar = 1, /datum/reagent/consumable/ice = 1)
mix_message = "You're suddenly reminded of home."
-/datum/chemical_reaction/arnold_palmer
+/datum/chemical_reaction/drink/arnold_palmer
results = list(/datum/reagent/consumable/tea/arnold_palmer = 2)
required_reagents = list(/datum/reagent/consumable/tea = 1, /datum/reagent/consumable/lemonade = 1)
mix_message = "The smells of fresh green grass and sand traps waft through the air as the mixture turns a friendly yellow-orange."
-/datum/chemical_reaction/chocolate_milk
+/datum/chemical_reaction/drink/chocolate_milk
results = list(/datum/reagent/consumable/milk/chocolate_milk = 5)
required_reagents = list(/datum/reagent/consumable/hot_coco = 3, /datum/reagent/consumable/coco = 2)
mix_message = "The color changes as the mixture blends smoothly."
required_temp = 300
is_cold_recipe = TRUE
+ optimal_temp = 280
+ overheat_temp = 5
+ thermic_constant= -1
-/datum/chemical_reaction/hot_coco
+/datum/chemical_reaction/drink/hot_coco
results = list(/datum/reagent/consumable/hot_coco = 6)
required_reagents = list(/datum/reagent/consumable/milk = 5, /datum/reagent/consumable/coco = 1)
required_temp = 320
-/datum/chemical_reaction/hot_coco_from_chocolate_milk
+/datum/chemical_reaction/drink/hot_coco_from_chocolate_milk
results = list(/datum/reagent/consumable/hot_coco = 3)
required_reagents = list(/datum/reagent/consumable/milk/chocolate_milk = 1, /datum/reagent/consumable/milk = 2)
required_temp = 320
-/datum/chemical_reaction/coffee
+/datum/chemical_reaction/drink/coffee
results = list(/datum/reagent/consumable/coffee = 5)
required_reagents = list(/datum/reagent/toxin/coffeepowder = 1, /datum/reagent/water = 5)
-/datum/chemical_reaction/tea
+/datum/chemical_reaction/drink/tea
results = list(/datum/reagent/consumable/tea = 5)
required_reagents = list(/datum/reagent/toxin/teapowder = 1, /datum/reagent/water = 5)
-/datum/chemical_reaction/eggnog
+/datum/chemical_reaction/drink/eggnog
results = list(/datum/reagent/consumable/ethanol/eggnog = 15)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 5, /datum/reagent/consumable/cream = 5, /datum/reagent/consumable/eggyolk = 5)
-/datum/chemical_reaction/narsour
+/datum/chemical_reaction/drink/narsour
results = list(/datum/reagent/consumable/ethanol/narsour = 1)
required_reagents = list(/datum/reagent/blood = 1, /datum/reagent/consumable/lemonjuice = 1, /datum/reagent/consumable/ethanol/demonsblood = 1)
mix_message = "The mixture develops a sinister glow."
mix_sound = 'sound/effects/singlebeat.ogg'
-/datum/chemical_reaction/quadruplesec
+/datum/chemical_reaction/drink/quadruplesec
results = list(/datum/reagent/consumable/ethanol/quadruple_sec = 15)
required_reagents = list(/datum/reagent/consumable/ethanol/triple_sec = 5, /datum/reagent/consumable/triple_citrus = 5, /datum/reagent/consumable/ethanol/creme_de_menthe = 5)
mix_message = "The snap of a taser emanates clearly from the mixture as it settles."
mix_sound = 'sound/weapons/taser.ogg'
-/datum/chemical_reaction/grasshopper
+/datum/chemical_reaction/drink/grasshopper
results = list(/datum/reagent/consumable/ethanol/grasshopper = 15)
required_reagents = list(/datum/reagent/consumable/cream = 5, /datum/reagent/consumable/ethanol/creme_de_menthe = 5, /datum/reagent/consumable/ethanol/creme_de_cacao = 5)
mix_message = "A vibrant green bubbles forth as the mixture emulsifies."
-/datum/chemical_reaction/stinger
+/datum/chemical_reaction/drink/stinger
results = list(/datum/reagent/consumable/ethanol/stinger = 15)
required_reagents = list(/datum/reagent/consumable/ethanol/cognac = 10, /datum/reagent/consumable/ethanol/creme_de_menthe = 5 )
-/datum/chemical_reaction/quintuplesec
+/datum/chemical_reaction/drink/quintuplesec
results = list(/datum/reagent/consumable/ethanol/quintuple_sec = 15)
required_reagents = list(/datum/reagent/consumable/ethanol/quadruple_sec = 5, /datum/reagent/consumable/clownstears = 5, /datum/reagent/consumable/ethanol/syndicatebomb = 5)
mix_message = "Judgement is upon you."
mix_sound = 'sound/items/airhorn2.ogg'
-/datum/chemical_reaction/bastion_bourbon
+/datum/chemical_reaction/drink/bastion_bourbon
results = list(/datum/reagent/consumable/ethanol/bastion_bourbon = 2)
required_reagents = list(/datum/reagent/consumable/tea = 1, /datum/reagent/consumable/ethanol/creme_de_menthe = 1, /datum/reagent/consumable/triple_citrus = 1, /datum/reagent/consumable/berryjuice = 1) //herbal and minty, with a hint of citrus and berry
mix_message = "You catch an aroma of hot tea and fruits as the mix blends into a blue-green color."
-/datum/chemical_reaction/squirt_cider
+/datum/chemical_reaction/drink/squirt_cider
results = list(/datum/reagent/consumable/ethanol/squirt_cider = 1)
required_reagents = list(/datum/reagent/water = 1, /datum/reagent/consumable/tomatojuice = 1, /datum/reagent/consumable/nutriment = 1)
mix_message = "The mix swirls and turns a bright red that reminds you of an apple's skin."
-/datum/chemical_reaction/fringe_weaver
+/datum/chemical_reaction/drink/fringe_weaver
results = list(/datum/reagent/consumable/ethanol/fringe_weaver = 10)
required_reagents = list(/datum/reagent/consumable/ethanol = 9, /datum/reagent/consumable/sugar = 1) //9 karmotrine, 1 adelhyde
mix_message = "The mix turns a pleasant cream color and foams up."
-/datum/chemical_reaction/sugar_rush
+/datum/chemical_reaction/drink/sugar_rush
results = list(/datum/reagent/consumable/ethanol/sugar_rush = 4)
required_reagents = list(/datum/reagent/consumable/sugar = 2, /datum/reagent/consumable/lemonjuice = 1, /datum/reagent/consumable/ethanol/wine = 1) //2 adelhyde (sweet), 1 powdered delta (sour), 1 karmotrine (alcohol)
mix_message = "The mixture bubbles and brightens into a girly pink."
-/datum/chemical_reaction/crevice_spike
+/datum/chemical_reaction/drink/crevice_spike
results = list(/datum/reagent/consumable/ethanol/crevice_spike = 6)
required_reagents = list(/datum/reagent/consumable/limejuice = 2, /datum/reagent/consumable/capsaicin = 4) //2 powdered delta (sour), 4 flanergide (spicy)
mix_message = "The mixture stings your eyes as it settles."
-/datum/chemical_reaction/sake
+/datum/chemical_reaction/drink/sake
results = list(/datum/reagent/consumable/ethanol/sake = 10)
required_reagents = list(/datum/reagent/consumable/rice = 10)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
mix_message = "The rice grains ferment into a clear, sweet-smelling liquid."
-/datum/chemical_reaction/peppermint_patty
+/datum/chemical_reaction/drink/peppermint_patty
results = list(/datum/reagent/consumable/ethanol/peppermint_patty = 10)
required_reagents = list(/datum/reagent/consumable/hot_coco = 6, /datum/reagent/consumable/ethanol/creme_de_cacao = 1, /datum/reagent/consumable/ethanol/creme_de_menthe = 1, /datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/menthol = 1)
mix_message = "The coco turns mint green just as the strong scent hits your nose."
-/datum/chemical_reaction/alexander
+/datum/chemical_reaction/drink/alexander
results = list(/datum/reagent/consumable/ethanol/alexander = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/cognac = 1, /datum/reagent/consumable/ethanol/creme_de_cacao = 1, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/sidecar
+/datum/chemical_reaction/drink/sidecar
results = list(/datum/reagent/consumable/ethanol/sidecar = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/cognac = 2, /datum/reagent/consumable/ethanol/triple_sec = 1, /datum/reagent/consumable/lemonjuice = 1)
-/datum/chemical_reaction/between_the_sheets
+/datum/chemical_reaction/drink/between_the_sheets
results = list(/datum/reagent/consumable/ethanol/between_the_sheets = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/ethanol/sidecar = 4)
-/datum/chemical_reaction/kamikaze
+/datum/chemical_reaction/drink/kamikaze
results = list(/datum/reagent/consumable/ethanol/kamikaze = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/vodka = 1, /datum/reagent/consumable/ethanol/triple_sec = 1, /datum/reagent/consumable/limejuice = 1)
-/datum/chemical_reaction/mojito
+/datum/chemical_reaction/drink/mojito
results = list(/datum/reagent/consumable/ethanol/mojito = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/rum = 1, /datum/reagent/consumable/sugar = 1, /datum/reagent/consumable/limejuice = 1, /datum/reagent/consumable/sodawater = 1, /datum/reagent/consumable/menthol = 1)
-/datum/chemical_reaction/fernet_cola
+/datum/chemical_reaction/drink/fernet_cola
results = list(/datum/reagent/consumable/ethanol/fernet_cola = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/fernet = 1, /datum/reagent/consumable/space_cola = 1)
-/datum/chemical_reaction/fanciulli
+/datum/chemical_reaction/drink/fanciulli
results = list(/datum/reagent/consumable/ethanol/fanciulli = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/manhattan = 1, /datum/reagent/consumable/ethanol/fernet = 1)
-/datum/chemical_reaction/branca_menta
+/datum/chemical_reaction/drink/branca_menta
results = list(/datum/reagent/consumable/ethanol/branca_menta = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/fernet = 1, /datum/reagent/consumable/ethanol/creme_de_menthe = 1, /datum/reagent/consumable/ice = 1)
-/datum/chemical_reaction/blank_paper
+/datum/chemical_reaction/drink/blank_paper
results = list(/datum/reagent/consumable/ethanol/blank_paper = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/silencer = 1, /datum/reagent/consumable/nothing = 1, /datum/reagent/consumable/nuka_cola = 1)
-/datum/chemical_reaction/wizz_fizz
+/datum/chemical_reaction/drink/wizz_fizz
results = list(/datum/reagent/consumable/ethanol/wizz_fizz = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/triple_sec = 1, /datum/reagent/consumable/sodawater = 1, /datum/reagent/consumable/ethanol/champagne = 1)
mix_message = "The beverage starts to froth with an almost mystical zeal!"
mix_sound = 'sound/effects/bubbles2.ogg'
-/datum/chemical_reaction/bug_spray
+/datum/chemical_reaction/drink/bug_spray
results = list(/datum/reagent/consumable/ethanol/bug_spray = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/triple_sec = 2, /datum/reagent/consumable/lemon_lime = 1, /datum/reagent/consumable/ethanol/rum = 2, /datum/reagent/consumable/ethanol/vodka = 1)
mix_message = "The faint aroma of summer camping trips wafts through the air; but what's that buzzing noise?"
mix_sound = 'sound/creatures/bee.ogg'
-/datum/chemical_reaction/jack_rose
+/datum/chemical_reaction/drink/jack_rose
results = list(/datum/reagent/consumable/ethanol/jack_rose = 4)
required_reagents = list(/datum/reagent/consumable/grenadine = 1, /datum/reagent/consumable/ethanol/applejack = 2, /datum/reagent/consumable/limejuice = 1)
mix_message = "As the grenadine incorporates, the beverage takes on a mellow, red-orange glow."
-/datum/chemical_reaction/turbo
+/datum/chemical_reaction/drink/turbo
results = list(/datum/reagent/consumable/ethanol/turbo = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/moonshine = 2, /datum/reagent/nitrous_oxide = 1, /datum/reagent/consumable/ethanol/sugar_rush = 1, /datum/reagent/consumable/pwr_game = 1)
-/datum/chemical_reaction/old_timer
+/datum/chemical_reaction/drink/old_timer
results = list(/datum/reagent/consumable/ethanol/old_timer = 6)
required_reagents = list(/datum/reagent/consumable/ethanol/whiskeysoda = 3, /datum/reagent/consumable/parsnipjuice = 2, /datum/reagent/consumable/ethanol/alexander = 1)
-/datum/chemical_reaction/rubberneck
+/datum/chemical_reaction/drink/rubberneck
results = list(/datum/reagent/consumable/ethanol/rubberneck = 10)
required_reagents = list(/datum/reagent/consumable/ethanol = 4, /datum/reagent/consumable/grey_bull = 5, /datum/reagent/consumable/astrotame = 1)
-/datum/chemical_reaction/duplex
+/datum/chemical_reaction/drink/duplex
results = list(/datum/reagent/consumable/ethanol/duplex = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/hcider = 2, /datum/reagent/consumable/applejuice = 1, /datum/reagent/consumable/berryjuice = 1)
-/datum/chemical_reaction/trappist
+/datum/chemical_reaction/drink/trappist
results = list(/datum/reagent/consumable/ethanol/trappist = 5)
required_reagents = list(/datum/reagent/consumable/ethanol/ale = 2, /datum/reagent/water/holywater = 2, /datum/reagent/consumable/sugar = 1)
-/datum/chemical_reaction/cream_soda
+/datum/chemical_reaction/drink/cream_soda
results = list(/datum/reagent/consumable/cream_soda = 4)
required_reagents = list(/datum/reagent/consumable/sugar = 2, /datum/reagent/consumable/sodawater = 2, /datum/reagent/consumable/vanilla = 1)
-/datum/chemical_reaction/blazaam
+/datum/chemical_reaction/drink/blazaam
results = list(/datum/reagent/consumable/ethanol/blazaam = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/gin = 2, /datum/reagent/consumable/peachjuice = 1, /datum/reagent/bluespace = 1)
-/datum/chemical_reaction/planet_cracker
+/datum/chemical_reaction/drink/planet_cracker
results = list(/datum/reagent/consumable/ethanol/planet_cracker = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/champagne = 2, /datum/reagent/consumable/ethanol/lizardwine = 2, /datum/reagent/consumable/eggyolk = 1, /datum/reagent/gold = 1)
mix_message = "The liquid's color starts shifting as the nanogold is alternately corroded and redeposited."
-/datum/chemical_reaction/red_queen
+/datum/chemical_reaction/drink/red_queen
results = list(/datum/reagent/consumable/red_queen = 10)
required_reagents = list(/datum/reagent/consumable/tea = 6, /datum/reagent/mercury = 2, /datum/reagent/consumable/blackpepper = 1, /datum/reagent/growthserum = 1)
-/datum/chemical_reaction/mauna_loa
+/datum/chemical_reaction/drink/mauna_loa
results = list(/datum/reagent/consumable/ethanol/mauna_loa = 5)
required_reagents = list(/datum/reagent/consumable/capsaicin = 2, /datum/reagent/consumable/ethanol/kahlua = 1, /datum/reagent/consumable/ethanol/bahama_mama = 2)
-/datum/chemical_reaction/godfather
+/datum/chemical_reaction/drink/godfather
results = list(/datum/reagent/consumable/ethanol/godfather = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/amaretto = 1, /datum/reagent/consumable/ethanol/whiskey = 1)
-/datum/chemical_reaction/godmother
+/datum/chemical_reaction/drink/godmother
results = list(/datum/reagent/consumable/ethanol/godmother = 2)
required_reagents = list(/datum/reagent/consumable/ethanol/amaretto = 1, /datum/reagent/consumable/ethanol/vodka = 1)
-/datum/chemical_reaction/amaretto_alexander
+/datum/chemical_reaction/drink/amaretto_alexander
results = list(/datum/reagent/consumable/ethanol/amaretto_alexander = 3)
required_reagents = list(/datum/reagent/consumable/ethanol/amaretto = 1, /datum/reagent/consumable/ethanol/creme_de_cacao = 1, /datum/reagent/consumable/cream = 1)
-/datum/chemical_reaction/ginger_amaretto
+/datum/chemical_reaction/drink/ginger_amaretto
results = list(/datum/reagent/consumable/ethanol/ginger_amaretto = 4)
required_reagents = list(/datum/reagent/consumable/ethanol/amaretto = 1, /datum/reagent/consumable/sol_dry = 1, /datum/reagent/consumable/ice = 1, /datum/reagent/consumable/lemonjuice = 1)
diff --git a/code/modules/food_and_drinks/recipes/food_mixtures.dm b/code/modules/food_and_drinks/recipes/food_mixtures.dm
index 2f1cc77cf6b4bb..eed688d1604dd1 100644
--- a/code/modules/food_and_drinks/recipes/food_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/food_mixtures.dm
@@ -8,39 +8,50 @@
//////////////////////////////////////////FOOD MIXTURES////////////////////////////////////
-/datum/chemical_reaction/tofu
+/datum/chemical_reaction/food
+ optimal_temp = 400
+ temp_exponent_factor = 1
+ optimal_ph_min = 2
+ optimal_ph_max = 10
+ thermic_constant = 0
+ H_ion_release = 0
+
+/datum/chemical_reaction/food/tofu
required_reagents = list(/datum/reagent/consumable/soymilk = 10)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/tofu/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/tofu/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/tofu(location)
return
-/datum/chemical_reaction/chocolatepudding
+/datum/chemical_reaction/food/chocolatepudding
results = list(/datum/reagent/consumable/chocolatepudding = 20)
required_reagents = list(/datum/reagent/consumable/milk/chocolate_milk = 10, /datum/reagent/consumable/eggyolk = 5)
-/datum/chemical_reaction/vanillapudding
+/datum/chemical_reaction/food/vanillapudding
results = list(/datum/reagent/consumable/vanillapudding = 20)
required_reagents = list(/datum/reagent/consumable/vanilla = 5, /datum/reagent/consumable/milk = 5, /datum/reagent/consumable/eggyolk = 5)
-/datum/chemical_reaction/chocolate_bar
+/datum/chemical_reaction/food/chocolate_bar
required_reagents = list(/datum/reagent/consumable/soymilk = 2, /datum/reagent/consumable/coco = 2, /datum/reagent/consumable/sugar = 2)
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/chocolate_bar/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/chocolate_bar/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/chocolatebar(location)
return
-/datum/chemical_reaction/chocolate_bar2
+/datum/chemical_reaction/food/chocolate_bar2
required_reagents = list(/datum/reagent/consumable/milk/chocolate_milk = 4, /datum/reagent/consumable/sugar = 2)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/chocolate_bar2/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/chocolate_bar2/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/chocolatebar(location)
@@ -49,7 +60,7 @@
/datum/chemical_reaction/chocolate_bar3
required_reagents = list(/datum/reagent/consumable/milk = 2, /datum/reagent/consumable/coco = 2, /datum/reagent/consumable/sugar = 2)
-/datum/chemical_reaction/chocolate_bar3/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/chocolate_bar3/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/chocolatebar(location)
@@ -59,110 +70,118 @@
results = list(/datum/reagent/consumable/soysauce = 5)
required_reagents = list(/datum/reagent/consumable/soymilk = 4, /datum/reagent/toxin/acid = 1)
-/datum/chemical_reaction/corn_syrup
+/datum/chemical_reaction/food/corn_syrup
results = list(/datum/reagent/consumable/corn_syrup = 5)
required_reagents = list(/datum/reagent/consumable/corn_starch = 1, /datum/reagent/toxin/acid = 1)
required_temp = 374
-/datum/chemical_reaction/caramel
+/datum/chemical_reaction/food/caramel
results = list(/datum/reagent/consumable/caramel = 1)
required_reagents = list(/datum/reagent/consumable/sugar = 1)
required_temp = 413.15
+ optimal_temp = 600
mob_react = FALSE
-/datum/chemical_reaction/caramel_burned
+/datum/chemical_reaction/food/caramel_burned
results = list(/datum/reagent/carbon = 1)
required_reagents = list(/datum/reagent/consumable/caramel = 1)
required_temp = 483.15
+ optimal_temp = 600
mob_react = FALSE
-/datum/chemical_reaction/cheesewheel
+/datum/chemical_reaction/food/cheesewheel
required_reagents = list(/datum/reagent/consumable/milk = 40)
required_catalysts = list(/datum/reagent/consumable/enzyme = 5)
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/cheesewheel/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/cheesewheel/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/cheese/wheel(location)
-/datum/chemical_reaction/synthmeat
+/datum/chemical_reaction/food/synthmeat
required_reagents = list(/datum/reagent/blood = 5, /datum/reagent/medicine/cryoxadone = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/synthmeat/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/synthmeat/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/meat/slab/synthmeat(location)
-/datum/chemical_reaction/hot_ramen
+/datum/chemical_reaction/food/hot_ramen
results = list(/datum/reagent/consumable/hot_ramen = 3)
required_reagents = list(/datum/reagent/water = 1, /datum/reagent/consumable/dry_ramen = 3)
-/datum/chemical_reaction/hell_ramen
+/datum/chemical_reaction/food/hell_ramen
results = list(/datum/reagent/consumable/hell_ramen = 6)
required_reagents = list(/datum/reagent/consumable/capsaicin = 1, /datum/reagent/consumable/hot_ramen = 6)
-/datum/chemical_reaction/imitationcarpmeat
+/datum/chemical_reaction/food/imitationcarpmeat
required_reagents = list(/datum/reagent/toxin/carpotoxin = 5)
required_container = /obj/item/food/tofu
mix_message = "The mixture becomes similar to carp meat."
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/imitationcarpmeat/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/food/imitationcarpmeat/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
new /obj/item/food/fishmeat/carp/imitation(location)
if(holder?.my_atom)
qdel(holder.my_atom)
-/datum/chemical_reaction/dough
+/datum/chemical_reaction/food/dough
required_reagents = list(/datum/reagent/water = 10, /datum/reagent/consumable/flour = 15)
mix_message = "The ingredients form a dough."
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/dough/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/dough/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/dough(location)
-/datum/chemical_reaction/cakebatter
+/datum/chemical_reaction/food/cakebatter
required_reagents = list(/datum/reagent/consumable/eggyolk = 15, /datum/reagent/consumable/flour = 15, /datum/reagent/consumable/sugar = 5)
mix_message = "The ingredients form a cake batter."
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/cakebatter/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/food/cakebatter/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/cakebatter(location)
-/datum/chemical_reaction/cakebatter/vegan
+/datum/chemical_reaction/food/cakebatter/vegan
required_reagents = list(/datum/reagent/consumable/soymilk = 15, /datum/reagent/consumable/flour = 15, /datum/reagent/consumable/sugar = 5)
-/datum/chemical_reaction/pancakebatter
+/datum/chemical_reaction/food/pancakebatter
results = list(/datum/reagent/consumable/pancakebatter = 15)
required_reagents = list(/datum/reagent/consumable/eggyolk = 12, /datum/reagent/consumable/milk = 10, /datum/reagent/consumable/flour = 5)
-/datum/chemical_reaction/ricebowl
+/datum/chemical_reaction/food/ricebowl
required_reagents = list(/datum/reagent/consumable/rice = 10, /datum/reagent/water = 10)
required_container = /obj/item/reagent_containers/glass/bowl
mix_message = "The rice absorbs the water."
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/ricebowl/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/food/ricebowl/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
new /obj/item/food/salad/ricebowl(location)
if(holder?.my_atom)
qdel(holder.my_atom)
-/datum/chemical_reaction/nutriconversion
+/datum/chemical_reaction/food/nutriconversion
results = list(/datum/reagent/consumable/nutriment/peptides = 0.5)
required_reagents = list(/datum/reagent/consumable/nutriment/ = 0.5)
required_catalysts = list(/datum/reagent/medicine/metafactor = 0.5)
-/datum/chemical_reaction/protein_peptide
+/datum/chemical_reaction/food/protein_peptide
results = list(/datum/reagent/consumable/nutriment/peptides = 0.5)
required_reagents = list(/datum/reagent/consumable/nutriment/protein = 0.5)
required_catalysts = list(/datum/reagent/medicine/metafactor = 0.5)
-/datum/chemical_reaction/bbqsauce
+/datum/chemical_reaction/food/bbqsauce
results = list(/datum/reagent/consumable/bbqsauce = 5)
required_reagents = list(/datum/reagent/ash = 1, /datum/reagent/consumable/tomatojuice = 1, /datum/reagent/medicine/salglu_solution = 3, /datum/reagent/consumable/blackpepper = 1)
-/datum/chemical_reaction/gravy
+/datum/chemical_reaction/food/gravy
results = list(/datum/reagent/consumable/gravy = 3)
required_reagents = list(/datum/reagent/consumable/milk = 1, /datum/reagent/consumable/nutriment = 1, /datum/reagent/consumable/flour = 1)
diff --git a/code/modules/jobs/job_types/chemist.dm b/code/modules/jobs/job_types/chemist.dm
index 0ca731d0b52ebc..e71e93c1cc5da5 100644
--- a/code/modules/jobs/job_types/chemist.dm
+++ b/code/modules/jobs/job_types/chemist.dm
@@ -27,6 +27,8 @@
glasses = /obj/item/clothing/glasses/science
belt = /obj/item/pda/chemist
+ l_pocket = /obj/item/reagent_containers/glass/bottle/random_buffer
+ r_pocket = /obj/item/reagent_containers/dropper
ears = /obj/item/radio/headset/headset_med
uniform = /obj/item/clothing/under/rank/medical/chemist
shoes = /obj/item/clothing/shoes/sneakers/white
diff --git a/code/modules/plumbing/plumbers/reaction_chamber.dm b/code/modules/plumbing/plumbers/reaction_chamber.dm
index 794987e5e044bb..75cfcfca2eb5b5 100644
--- a/code/modules/plumbing/plumbers/reaction_chamber.dm
+++ b/code/modules/plumbing/plumbers/reaction_chamber.dm
@@ -13,6 +13,11 @@
///our reagent goal has been reached, so now we lock our inputs and start emptying
var/emptying = FALSE
+ ///towards which temperature do we build (except during draining)?
+ var/target_temperature = 300
+ ///cool/heat power
+ var/heater_coefficient = 0.05 //same lvl as acclimator
+
/obj/machinery/plumbing/reaction_chamber/Initialize(mapload, bolt)
. = ..()
AddComponent(/datum/component/plumbing/reaction_chamber, bolt)
@@ -37,6 +42,11 @@
holder.flags |= NO_REACT
return NONE
+/obj/machinery/plumbing/reaction_chamber/process(delta_time)
+ if(!emptying) //suspend heating/cooling during emptying phase
+ reagents.adjust_thermal_energy((target_temperature - reagents.chem_temp) * heater_coefficient * delta_time * SPECIFIC_HEAT_DEFAULT * reagents.total_volume) //keep constant with chem heater
+ reagents.handle_reactions()
+
/obj/machinery/plumbing/reaction_chamber/power_change()
. = ..()
if(use_power != NO_POWER_USE)
@@ -59,6 +69,10 @@
data["reagents"] = text_reagents
data["emptying"] = emptying
+ data["temperature"] = round(reagents.chem_temp, 0.1)
+ data["ph"] = round(reagents.ph, 0.01)
+ data["targetTemp"] = target_temperature
+ data["isReacting"] = reagents.is_reacting
return data
/obj/machinery/plumbing/reaction_chamber/ui_act(action, params)
@@ -77,3 +91,10 @@
var/input_amount = text2num(params["amount"])
if(input_amount)
required_reagents[input_reagent] = input_amount
+ if("temperature")
+ var/target = params["target"]
+ if(text2num(target) != null)
+ target = text2num(target)
+ . = TRUE
+ if(.)
+ target_temperature = clamp(target, 0, 1000)
diff --git a/code/modules/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm
new file mode 100644
index 00000000000000..e44ddbc53f0a77
--- /dev/null
+++ b/code/modules/reagents/chemistry/equilibrium.dm
@@ -0,0 +1,404 @@
+/*
+* #/datum/equilibrium
+*
+* A dynamic reaction object that processes the reaction that it is set within it. Relies on a reagents holder to call and operate the functions.
+*
+* An object/datum to contain the vars for each of the reactions currently ongoing in a holder/reagents datum
+* This way all information is kept within one accessable object
+* equilibrium is a unique name as reaction is already too close to chemical_reaction
+* This is set up this way to reduce holder.dm bloat as well as reduce confusing list overhead
+* The crux of the fermimechanics are handled here
+* Instant reactions AREN'T handled here. See holder.dm
+*/
+/datum/equilibrium
+ ///The chemical reaction that is presently being processed
+ var/datum/chemical_reaction/reaction
+ ///The location/reagents datum the processing is taking place
+ var/datum/reagents/holder
+ ///How much product we can make multiplied by the input recipe's products/required_reagents numerical values
+ var/multiplier = INFINITY
+ ///The sum total of each of the product's numerical's values. This is so the addition/deletion is kept at the right values for multiple product reactions
+ var/product_ratio = 0
+ ///The total possible that this reaction can make presently - used for gui outputs
+ var/target_vol = 0
+ ///The target volume the reaction is headed towards. This is updated every tick, so isn't the total value for the reaction, it's just a way to ensure we can't make more than is possible.
+ var/step_target_vol = INFINITY
+ ///How much of the reaction has been made so far. Mostly used for subprocs, but it keeps track across the whole reaction and is added to every step.
+ var/reacted_vol = 0
+ ///What our last delta_ph was
+ var/reaction_quality = 1
+ ///If we're done with this reaction so that holder can clear it.
+ var/to_delete = FALSE
+ ///Result vars, private - do not edit unless in reaction_step()
+ ///How much we're adding
+ var/delta_t
+ ///How pure our step is
+ var/delta_ph
+ ///Modifiers from catalysts, do not use negative numbers.
+ ///I should write a better handiler for modifying these
+ ///Speed mod
+ var/speed_mod = 1
+ ///pH mod
+ var/h_ion_mod = 1
+ ///Temp mod
+ var/thermic_mod = 1
+ ///Allow us to deal with lag by "charging" up our reactions to react faster over a period - this means that the reaction doesn't suddenly mass react - which can cause explosions
+ var/time_deficit
+
+/*
+* Creates and sets up a new equlibrium object
+*
+* Arguments:
+* * input_reaction - the chemical_reaction datum that will be processed
+* * input_holder - the reagents datum that the output will be put into
+*/
+/datum/equilibrium/New(datum/chemical_reaction/input_reaction, datum/reagents/input_holder)
+ reaction = input_reaction
+ holder = input_holder
+ if(!holder || !reaction) //sanity check
+ stack_trace("A new [type] was set up, with incorrect/null input vars!")
+ to_delete = TRUE
+ return
+ if(!check_inital_conditions()) //If we're outside of the scope of the reaction vars
+ to_delete = TRUE
+ return
+ if(!length(reaction.results)) //Come back to and revise the affected reactions in the next PR, this is a placeholder fix.
+ holder.instant_react(reaction) //Even if this check fails, there's a backup - look inside of calculate_yield()
+ to_delete = TRUE
+ return
+ LAZYADD(holder.reaction_list, src)
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] attempts")
+
+
+/datum/equilibrium/Destroy()
+ if(reacted_vol < target_vol) //We did NOT finish from reagents - so we can restart this reaction given property changes in the beaker. (i.e. if it stops due to low temp, this will allow it to fast restart when heated up again)
+ LAZYADD(holder.failed_but_capable_reactions, reaction) //Consider replacing check with calculate_yield()
+ LAZYREMOVE(holder.reaction_list, src)
+ holder = null
+ reaction = null
+ return ..()
+
+/*
+* Check to make sure our input vars are sensible - truncated version of check_reagent_properties()
+*
+* (as the setup in holder.dm checks for that already - this is a way to reduce calculations on New())
+* Don't call this unless you know what you're doing, this is an internal proc
+*/
+/datum/equilibrium/proc/check_inital_conditions()
+ //Make sure we have the right multipler for on_reaction()
+ for(var/single_reagent in reaction.required_reagents)
+ multiplier = min(multiplier, round((holder.get_reagent_amount(single_reagent) / reaction.required_reagents[single_reagent]), CHEMICAL_QUANTISATION_LEVEL))
+ if(multiplier == INFINITY)
+ return FALSE
+ //Consider purity gating too? - probably not, purity is hard to determine
+ //To prevent reactions outside of the pH window from starting.
+ if(!((holder.ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (holder.ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
+ return FALSE
+ return TRUE
+
+/*
+* Check to make sure our input vars are sensible - is the holder overheated? does it have the required reagents? Does it have the required calalysts?
+*
+* If you're adding more checks for reactions, this is the proc to edit
+* otherwise, generally, don't call this directed except internally
+*/
+/datum/equilibrium/proc/check_reagent_properties()
+ //Have we exploded from on_reaction?
+ if(!holder.my_atom || holder.reagent_list.len == 0)
+ return FALSE
+ if(!holder)
+ stack_trace("an equilibrium is missing it's holder.")
+ return FALSE
+ if(!reaction)
+ stack_trace("an equilibrium is missing it's reaction.")
+ return FALSE
+
+ //set up catalyst checks
+ var/total_matching_catalysts = 0
+ //Reagents check should be handled in the calculate_yield() from multiplier
+
+ //If the product/reactants are too impure
+ for(var/r in holder.reagent_list)
+ var/datum/reagent/reagent = r
+ //this is done this way to reduce processing compared to holder.has_reagent(P)
+ for(var/c in reaction.required_catalysts)
+ var/datum/reagent/catalyst = c
+ if(catalyst == reagent.type)
+ total_matching_catalysts++
+ if(istype(reagent, /datum/reagent/catalyst_agent))
+ var/datum/reagent/catalyst_agent/catalyst_agent = reagent
+ if(reagent.volume >= catalyst_agent.min_volume)
+ catalyst_agent.consider_catalyst(src)
+
+ if(!(total_matching_catalysts == reaction.required_catalysts.len))
+ return FALSE
+
+ //All good!
+ return TRUE
+
+/*
+* Calculates how much we're aiming to create
+*
+* Specifically calcuates multiplier, product_ratio, step_target_vol
+* Also checks to see if these numbers are sane, returns a TRUE/FALSE
+* Generally an internal proc
+*/
+/datum/equilibrium/proc/calculate_yield()
+ if(!reaction)
+ stack_trace("Tried to calculate an equlibrium for reaction [reaction.type], but there was no reaction set for the datum")
+ return FALSE
+
+ multiplier = INFINITY
+ for(var/reagent in reaction.required_reagents)
+ multiplier = min(multiplier, round((holder.get_reagent_amount(reagent) / reaction.required_reagents[reagent]), CHEMICAL_QUANTISATION_LEVEL))
+
+ if(!length(reaction.results)) //Incase of no reagent product
+ product_ratio = 1
+ step_target_vol = INFINITY
+ for(var/reagent in reaction.required_reagents)
+ step_target_vol = min(step_target_vol, multiplier * reaction.required_reagents[reagent])
+ if(step_target_vol == 0 || multiplier == 0)
+ return FALSE
+ //Sanity Check
+ if(step_target_vol == INFINITY || multiplier == INFINITY) //I don't see how this can happen, but I'm not bold enough to let infinities roll around for free
+ to_delete = TRUE
+ CRASH("Tried to calculate target vol for [reaction.type] with no products, but could not find required reagents for the reaction. If it got here, something is really broken with the recipe.")
+ return TRUE
+
+ product_ratio = 0
+ step_target_vol = 0
+ var/true_reacted_vol //Because volumes can be lost mid reactions
+ for(var/product in reaction.results)
+ step_target_vol += (reaction.results[product]*multiplier)
+ product_ratio += reaction.results[product]
+ true_reacted_vol += holder.get_reagent_amount(product)
+ if(step_target_vol == 0 || multiplier == INFINITY)
+ return FALSE
+ target_vol = step_target_vol + true_reacted_vol
+ reacted_vol = true_reacted_vol
+ return TRUE
+
+/*
+* Deals with lag - allows a reaction to speed up to 3x from delta_time
+* "Charged" time (time_deficit) discharges by incrementing reactions by doubling them
+* If delta_time is greater than 1.5, then we save the extra time for the next ticks
+*
+* Arguments:
+* * delta_time - the time between the last proc in world.time
+*/
+/datum/equilibrium/proc/deal_with_time(delta_time)
+ if(delta_time > 1)
+ time_deficit += delta_time - 1
+ delta_time = 1 //Lets make sure reactions aren't super speedy and blow people up from a big lag spike
+ else if (time_deficit)
+ if(time_deficit < 0.25)
+ delta_time += time_deficit
+ time_deficit = 0
+ else
+ delta_time += 0.25
+ time_deficit = 0.25
+ return delta_time
+
+/*
+* Main method of checking for explosive - or failed states
+* Checks overheated() and overly_impure() of a reaction
+* This was moved from the start, to the end - after a reaction, so post reaction temperature changes aren't ignored.
+* overheated() is first - so double explosions can't happen (i.e. explosions that blow up the holder)
+*/
+/datum/equilibrium/proc/check_fail_states()
+ //Are we overheated?
+ if(reaction.is_cold_recipe)
+ if(holder.chem_temp < reaction.overheat_temp) //This is before the process - this is here so that overly_impure and overheated() share the same code location (and therefore vars) for calls.
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
+ reaction.overheated(holder, src)
+ else
+ if(holder.chem_temp > reaction.overheat_temp)
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
+ reaction.overheated(holder, src)
+
+ //is our product too impure?
+ for(var/product in reaction.results)
+ var/datum/reagent/reagent = holder.has_reagent(product)
+ if(!reagent) //might be missing from overheat exploding
+ continue
+ if (reagent.purity < reaction.purity_min)//If purity is below the min, call the proc
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overly impure reaction steps")
+ reaction.overly_impure(holder, src)
+
+ //did we explode?
+ if(!holder.my_atom || holder.reagent_list.len == 0)
+ return FALSE
+ return TRUE
+
+/*
+* Main reaction processor - Increments the reaction by a timestep
+*
+* First checks the holder to make sure it can continue
+* Then calculates the purity and volume produced.TRUE
+* Then adds/removes reagents
+* Then alters the holder pH and temperature, and calls reaction_step
+* Arguments:
+* * delta_time - the time displacement between the last call and the current, 1 is a standard step
+* * purity_modifier - how much to modify the step's purity by (0 - 1)
+*/
+/datum/equilibrium/proc/react_timestep(delta_time, purity_modifier = 1)
+ if(to_delete)
+ //This occurs when it explodes
+ return FALSE
+ if(!check_reagent_properties()) //this is first because it'll call explosions first
+ to_delete = TRUE
+ return
+ if(!calculate_yield())//So that this can detect if we're missing reagents
+ to_delete = TRUE
+ return
+ delta_time = deal_with_time(delta_time)
+
+ delta_t = 0 //how far off optimal temp we care
+ delta_ph = 0 //How far off the pH we are
+ var/cached_ph = holder.ph
+ var/cached_temp = holder.chem_temp
+ var/purity = 1 //purity of the current step
+
+ //Begin checks
+ //Calculate DeltapH (Deviation of pH from optimal)
+ //Within mid range
+ if (cached_ph >= reaction.optimal_ph_min && cached_ph <= reaction.optimal_ph_max)
+ delta_ph = 1 //100% purity for this step
+ //Lower range
+ else if (cached_ph < reaction.optimal_ph_min) //If we're outside of the optimal lower bound
+ if (cached_ph < (reaction.optimal_ph_min - reaction.determin_ph_range)) //If we're outside of the deterministic bound
+ delta_ph = 0 //0% purity
+ else //We're in the deterministic phase
+ delta_ph = (((cached_ph - (reaction.optimal_ph_min - reaction.determin_ph_range))**reaction.ph_exponent_factor)/((reaction.determin_ph_range**reaction.ph_exponent_factor))) //main pH calculation
+ //Upper range
+ else if (cached_ph > reaction.optimal_ph_max) //If we're above of the optimal lower bound
+ if (cached_ph > (reaction.optimal_ph_max + reaction.determin_ph_range)) //If we're outside of the deterministic bound
+ delta_ph = 0 //0% purity
+ else //We're in the deterministic phase
+ delta_ph = (((- cached_ph + (reaction.optimal_ph_max + reaction.determin_ph_range))**reaction.ph_exponent_factor)/(reaction.determin_ph_range**reaction.ph_exponent_factor))//Reverse - to + to prevent math operation failures.
+
+ //This should never proc, but it's a catch incase someone puts in incorrect values
+ else
+ stack_trace("[holder.my_atom] attempted to determine FermiChem pH for '[reaction.type]' which had an invalid pH of [cached_ph] for set recipie pH vars. It's likely the recipe vars are wrong.")
+
+ //Calculate DeltaT (Deviation of T from optimal)
+ if(!reaction.is_cold_recipe)
+ if (cached_temp < reaction.optimal_temp && cached_temp >= reaction.required_temp)
+ delta_t = (((cached_temp - reaction.required_temp)**reaction.temp_exponent_factor)/((reaction.optimal_temp - reaction.required_temp)**reaction.temp_exponent_factor))
+ else if (cached_temp >= reaction.optimal_temp)
+ delta_t = 1
+ else //too hot
+ delta_t = 0
+ to_delete = TRUE
+ return
+ else
+ if (cached_temp > reaction.optimal_temp && cached_temp <= reaction.required_temp)
+ delta_t = (((cached_temp - reaction.required_temp)**reaction.temp_exponent_factor)/((reaction.optimal_temp - reaction.required_temp)**reaction.temp_exponent_factor))
+ else if (cached_temp <= reaction.optimal_temp)
+ delta_t = 1
+ else //Too cold
+ delta_t = 0
+ to_delete = TRUE
+ return
+
+ //Call any special reaction steps BEFORE addition
+ if(reaction.reaction_step(src, holder, delta_t, delta_ph, step_target_vol) == END_REACTION)
+ to_delete = TRUE
+ return
+
+ //Catalyst modifier
+ delta_t *= speed_mod
+
+ purity = delta_ph//set purity equal to pH offset
+
+ //Then adjust purity of result with beaker reagent purity.
+ purity *= reactant_purity(reaction)
+
+ //Then adjust it from the input modifier
+ purity *= purity_modifier
+
+ //Now we calculate how much to add - this is normalised to the rate up limiter
+ var/delta_chem_factor = (reaction.rate_up_lim*delta_t)*delta_time//add/remove factor
+ var/total_step_added = 0
+ //keep limited
+ if(delta_chem_factor > step_target_vol)
+ delta_chem_factor = step_target_vol
+ else if (delta_chem_factor < CHEMICAL_VOLUME_MINIMUM)
+ delta_chem_factor = CHEMICAL_VOLUME_MINIMUM
+ //Normalise to multiproducts
+ delta_chem_factor /= product_ratio
+ //delta_chem_factor = round(delta_chem_factor, CHEMICAL_QUANTISATION_LEVEL) // Might not be needed - left here incase testmerge shows that it does. Remove before full commit.
+
+ //Calculate how much product to make and how much reactant to remove factors..
+ for(var/reagent in reaction.required_reagents)
+ holder.remove_reagent(reagent, (delta_chem_factor * reaction.required_reagents[reagent]), safety = TRUE)
+ //Apply pH changes
+ holder.adjust_specific_reagent_ph(reagent, (delta_chem_factor * reaction.required_reagents[reagent])*(reaction.H_ion_release*h_ion_mod))
+
+ var/step_add
+ for(var/product in reaction.results)
+ //create the products
+ step_add = delta_chem_factor * reaction.results[product]
+ holder.add_reagent(product, step_add, null, cached_temp, purity, override_base_ph = TRUE)
+ //Apply pH changes
+ holder.adjust_specific_reagent_ph(product, step_add*reaction.H_ion_release)
+ reacted_vol += step_add
+ total_step_added += step_add
+
+ #ifdef REAGENTS_TESTING //Kept in so that people who want to write fermireactions can contact me with this log so I can help them
+ if(GLOB.Debug2) //I want my spans for my sanity
+ message_admins("Reaction step active for:[reaction.type]")
+ message_admins("|Reaction conditions| Temp: [holder.chem_temp], pH: [holder.ph], reactions: [length(holder.reaction_list)], awaiting reactions: [length(holder.failed_but_capable_reactions)], no. reagents:[length(holder.reagent_list)], no. prev reagents: [length(holder.previous_reagent_list)]")
+ message_admins("Reaction vars: PreReacted:[reacted_vol] of [step_target_vol] of total [target_vol]. delta_t [delta_t], multiplier [multiplier], delta_chem_factor [delta_chem_factor] Pfactor [product_ratio], purity of [purity] from a delta_ph of [delta_ph]. DeltaTime: [delta_time]")
+ #endif
+
+ //Apply thermal output of reaction to beaker
+ if(reaction.reaction_flags & REACTION_HEAT_ARBITARY)
+ holder.chem_temp += (reaction.thermic_constant* total_step_added*thermic_mod) //old method - for every bit added, the whole temperature is adjusted
+ else //Standard mechanics
+ var/heat_energy = reaction.thermic_constant * total_step_added * thermic_mod * SPECIFIC_HEAT_DEFAULT
+ holder.adjust_thermal_energy(heat_energy, 0, 10000) //heat is relative to the beaker conditions
+
+ //Give a chance of sounds
+ if(prob(5))
+ holder.my_atom.audible_message("[icon2html(holder.my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [reaction.mix_message]")
+ if(reaction.mix_sound)
+ playsound(get_turf(holder.my_atom), reaction.mix_sound, 80, TRUE)
+
+ //Used for UI output
+ reaction_quality = purity
+
+ //post reaction checks
+ if(!(check_fail_states()))
+ to_delete = TRUE
+
+ //end reactions faster so plumbing is faster
+ if((step_add >= step_target_vol) && (length(holder.reaction_list == 1)))//length is so that plumbing is faster - but it doesn't disable competitive reactions. Basically, competitive reactions will likely reach their step target at the start, so this will disable that. We want to avoid that. But equally, we do want to full stop a holder from reacting asap so plumbing isn't waiting an tick to resolve.
+ to_delete = TRUE
+
+ holder.update_total()//do NOT recalculate reactions
+
+
+/*
+* Calculates the total sum normalised purity of ALL reagents in a holder
+*
+* Currently calculates it irrespective of required reagents at the start, but this should be changed if this is powergamed to required reagents
+* It's not currently because overly_impure affects all reagents
+*/
+/datum/equilibrium/proc/reactant_purity(datum/chemical_reaction/C)
+ var/list/cached_reagents = holder.reagent_list
+ var/i = 0
+ var/cached_purity
+ for(var/datum/reagent/reagent as anything in holder.reagent_list)
+ if (reagent in cached_reagents)
+ cached_purity += reagent.purity
+ i++
+ if(!i)//I've never seen it get here with 0, but in case - it gets here when it blows up from overheat
+ stack_trace("No reactants found mid reaction for [C.type]. Beaker: [holder.my_atom]")
+ return 0 //we exploded and cleared reagents - but lets not kill the process
+ return cached_purity/i
+
+///Panic stop a reaction - cleanup should be handled by the next timestep
+/datum/equilibrium/proc/force_clear_reactive_agents()
+ for(var/reagent in reaction.required_reagents)
+ holder.remove_reagent(reagent, (multiplier * reaction.required_reagents[reagent]), safety = 1)
diff --git a/code/modules/reagents/chemistry/fermi_readme.md b/code/modules/reagents/chemistry/fermi_readme.md
new file mode 100644
index 00000000000000..ebb930e486fca0
--- /dev/null
+++ b/code/modules/reagents/chemistry/fermi_readme.md
@@ -0,0 +1,212 @@
+# How to make fermi reactions from a code perspective
+
+## How purity works
+
+Purity by default only splits on a mob’s consumption unless reaction_flags in the recipe is set to one of the CLEAR_CONVERT defines. Here is a bad flowchart for the on mob process.
+
+I am not good at flowcharts sorry.
+
+![image](https://user-images.githubusercontent.com/33956696/103941231-78123b80-5126-11eb-9d89-635a6b810454.png)
+
+Essentially
+For purity:
+If 1: normal
+If above inverse_chem_val: normal + impure
+If below: inverse
+
+# How reactions mechanics work
+
+For the effects starting/during/at the end of a reaction see below:
+
+![image](https://user-images.githubusercontent.com/33956696/103941281-88c2b180-5126-11eb-8740-207dc9bb830d.png)
+
+Maybe this makes no sense.
+
+In brief:
+
+Holder.dm now sets up reactions, while equilibrium.dm runs them. Holder itself is processed when there is a list of reactions, but the equilibrium does the calculating. In essence, it holds onto a list of objects to run. Handle_reactions() is used to update the reaction list, with a few checks at the start to prevent any unnecessary updates.
+
+#### When a reaction is detected:
+- If it’s REACTION_INSTANT then it’ll use a method similar to the old mechanics.
+- If not then it’ll set up an equilibrium, which checks to see if the reaction is valid on creation.
+- If it’s valid, then on_reaction is called.
+- If the reaction’s temperature is over the overheat_temp overheated() is called
+- When equilibriums detect they’re invalid, they flag for deletion and holder.dm deletes them.
+- If there’s a list of reactions, then the holder starts processing.
+
+#### When holder is processing:
+- Each equilibrium is processed, and it handles it’s own reaction. For each step it handles every reaction.
+- At the start, the equilibrium checks it’s conditions and calculates how much it can make in this step.
+- It checks the temp, reagents and catalyst.
+- If it’s overheated, call overheated()
+- If it’s too impure call overly_impure()
+- The offset of optimal pH and temp is calculated, and these correlate with purity and yield.
+
+#### How a holder stops reacting:
+When one of the checks fails in the equilibrium object, it is flagged for deletion. The holder will detect this and call reaction_finish() and delete the equilibrium object – ending that reaction.
+
+## Recipe and processing mechanics
+
+Lets go over the reaction vars below. These can be edited and set on a per chemical_reaction basis
+```dm
+/datum/chemical_reaction
+ ...
+ var/required_temp = 100
+ var/optimal_temp = 500 // Upper end for above
+ var/overheat_temp = 900 // Temperature at which reaction explodes - If any reaction is this hot, it procs overheated()
+ var/optimal_ph_min = 5 // Lowest value of pH determining pH a 1 value for pH based rate reactions (Plateu phase)
+ var/optimal_ph_max = 9 // Higest value for above
+ var/determin_ph_range = 4 // How far out pH wil react, giving impurity place (Exponential phase)
+ var/temp_exponent_factor = 2 // How sharp the temperature exponential curve is (to the power of value)
+ var/ph_exponent_factor = 1 // How sharp the pH exponential curve is (to the power of value)
+ var/thermic_constant = 1 // Temperature change per 1u produced
+ var/H_ion_release = 0.01 // pH change per 1u reaction
+ var/rate_up_lim = 20 // Optimal/max rate possible if all conditions are perfect
+ var/purity_min = 0.15 // If purity is below 0.15, it calls OverlyImpure() too. Set to 0 to disable this.
+ var/reaction_flags // bitflags for clear conversions; REACTION_CLEAR_IMPURE, REACTION_CLEAR_INVERSE, REACTION_CLEAR_RETAIN, REACTION_INSTANT
+```
+
+### How temperature ranges are set and how reaction rate is determined
+
+Reaction rates are determined by the current temperature of the reagents holder. There are a few variables related to this:
+
+```dm
+/datum/chemical_reaction
+ var/required_temp = 100
+ var/optimal_temp = 500 // Upper end for above
+ var/overheat_temp = 900 // Temperature at which reaction explodes - If any reaction is this hot, it procs overheated()
+ var/temp_exponent_factor = 2 // How sharp the temperature exponential curve is (to the power of value)
+ var/rate_up_lim = 20 // Optimal/max rate possible if all conditions are perfect
+```
+
+The amount added is based off the recipies’ required_temp, optimal_temp, overheat_temp and temp_exponent_factor. See below:
+![image](https://user-images.githubusercontent.com/33956696/104081088-5e571e00-5224-11eb-8834-87aa36b3e45f.png)
+
+the y axis is the normalised value of growth, which is then muliplied by the rate_up_lim. You can see that temperatures below the required_temp produce no result (the reaction doesn't start, or if it is reacting, the reaction will stop). Between the required and optimal is a region that is defined by the temp_exponent_factor, so in this case the value is ^2, so we see exponential growth. Between the optimal_temp and the overheat_temp is the optimal phase - where the rate factor is 1. After that it continues to react, but will call overheated() per timestep. Presently the default for overheated() is to reduce the yield of the product (i.e. it's faster but you get less). The rate_up_lim is the maximum rate the reaction can go at optimal temperatures, so in this case a rate factor of 1 i.e. a temperature between 500+ will produce 10u, or a temperature of 400 will roughly produce 4u per step (independant of product ratio produced, if you put 10, it will only create 10 maximum regardless of how much product is defined in the results list).
+
+### How pH ranges are set and what pH mechanics do
+
+Optimal pH ranges are set on a per recipe basis - though at the moment all recipes use a default recipe, so they all have the same window (except for the buffers). Hopefully either as a community effort/or in future PRs we can create unique profiles for the present reactions in the game.
+
+As for how you define the reaction variables for a reaction, there are a few new variables for the chemical_recipe datum. I'll go over specifically how pH works for the default reaction.
+```dm
+/datum/chemical_reaction
+ ...
+ var/optimal_ph_min = 5 // Lowest value of pH determining pH a 1 value for pH based rate reactions (Plateu phase)
+ var/optimal_ph_max = 9 // Higest value for above
+ var/determin_ph_range = 4 // How far out pH wil react, giving impurity place (Exponential phase)
+ var/ph_exponent_factor = 1 // How sharp the pH exponential curve is (to the power of value)
+ var/purity_min = 0.15 // If purity is below 0.15, it calls overly_impure(). In addition, if the product's purity is below this value at the end, the product will be 100% converted into the reagent's failed_chem. Set to 0 to disable this.
+```
+
+For this default reaction, the curve looks like this:
+![image](https://user-images.githubusercontent.com/33956696/104081030-0fa98400-5224-11eb-822e-ffc614799ddc.png)
+
+The y axis is the purity of the product made for that time step. This is recalculated based off the beaker's sum pH for every tick in the reaction. The rate in which your product is made based off the temperature (If you want me to describe that too I can.) So say our reaction has 10u of a purity 1 of product in there, and for our step we're making another 10u with our pH at (roughly) 3, from the curve our purity is (roughly) 0.5. So we will be adding 10u of 0.5 purity to 10u of 1 purity, resulting in 20u of 0.75 purity product. (Though - to note the reactant's purities also modify the purity of volume created on top of this).
+
+If you're designing a reaction you can define an optimal range between the OptimalpHMin to OptimalpHMax (5 - 7 in this case) and a deterministic region set by the ReactpHLim (5 - 4, 9 + 4 aka between 1 to 5 and 9 to 13). This deterministic region is exponential, so if you set it to 2 then it’ll exponentially grow, but since our CurveSharpph = 1, it’s linear (basically normalise the range in the determinsitic region, then put that to the power of CurveSharppH). Finally values outside of these ranges will prevent reactions from starting, but if a reaction drifts out during a reaction, the purity of volume created for each step will be 0 (It does not stop ongoing reactions). It’s entirely possible to design a reaction without a deterministic or optimal phase if you wanted.
+
+Though to note; if your purity dips below the PurityMin of a reaction it’ll call the overly_impure() function – which by default reduces the purity of all reagents in the beaker. Additionally, if the purity at the end of a reaction is below the PurityMin, it’ll convert into the failed chem defined by the product’s failed_chem defined in it's reagent datum. For default the PurityMin is 0.15, and is pretty difficult to fail. This is all customisable however, if you wanted to use these hooks to design a even more unique reaction, just don’t call the parent proc when using methods.
+
+
+### Conditional changes in reagents datum per timestep
+
+```dm
+/datum/chemical_reaction
+ ...
+ var/thermic_constant = 1 // Temperature change per 1u produced
+ var/H_ion_release = 0.01 // pH change per 1u reaction
+```
+
+The thermic_constant is how much the temperature changes per u created, so for 10u created the temperature will increase by 10K. The H_ion_release is how much the pH changes per u created, for 10u created the pH will increase by 0.1. During a reaction this is the only factor in pH changes - presently the addition/removal of reagents tie to the reaction won't affect this, though other reactions ongoing in the beaker will also affect pH, as well as the removal/addition of reagents outside of the reaction.
+
+### Reaction flags
+
+Reaction_flags can be used to set these defines:
+
+```dm
+#define REACTION_CLEAR_IMPURE //Convert into impure/pure on reaction completion in the datum/reagents holder instead of on consumption
+#define REACTION_CLEAR_INVERSE //Convert into inverse on reaction completion when purity is low enough in the datum/reagents holder instead of on consumption
+#define REACTION_CLEAR_RETAIN //Clear converted chems retain their purities/inverted purities. Requires 1 or both of the above. This is so that it can split again after splitting from a reaction (i.e. if your impure_chem or inverse_chem has it's own impure_chem/inverse_chem and you want it to split again on consumption).
+#define REACTION_INSTANT //Used to create instant reactions
+
+/datum/chemical_reaction
+ var/reaction_flags
+```
+
+For REACTION_CLEAR – this causes the purity mechanics to resolve in the beaker at the end of the reaction, instead of when added to a mob.
+
+#### A note on cold recipies
+
+Is_cold_recipie requires you to set your overheat_temp and optimal_temp descend instead.
+Eg:
+```dm
+/datum/chemical_reaction
+ ...
+ var/required_temp = 300
+ var/optimal_temp = 200
+ var/overheat_temp = 50
+```
+
+# Reagents
+The new vars that are introduced are below:
+```dm
+/datum/reagent
+ /// pH of the reagent
+ var/ph = 7
+ ///Purity of the reagent
+ 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
+ //impure chem values (see fermi_readme.dm for more details):
+ var/impure_chem = /datum/reagent/impurity // What chemical path is made when metabolised as a function of purity
+ var/inverse_chem_val = 0.2 // If the impurity is below 0.5, replace ALL of the chem with inverse_chem upon metabolising
+ var/inverse_chem = /datum/reagent/impurity/toxic // What chem is metabolised when purity is below inverse_chem_val
+ var/failed_chem = /datum/reagent/consumable/failed_reaction //what chem is made at the end of a reaction IF the purity is below the recipies purity_min
+ var/chemical_flags
+```
+
+- `pH` is the innate pH of the reagent and is used to calculate the pH of a reagents datum on addition/removal. This does not change and is a reference value. The reagents datum pH changes.
+- `purity` is the INTERNAL value for splitting. This is set to 1 after splitting so that it doesn't infinite split
+- `creation_purity` is the purity of the reagent on creation. This won't change. If you want to write code that checks the purity in any of the methods, use this.
+- `impure_chem` is the datum type that is created provided that it's `creation_purity` is above the `inverse_chem_val`. When the reagent is consumed it will split into this OR if the associated `datum/chemical_recipe` has a REACTION_CLEAR_IMPURE flag it will split at the end of the reaction in the `datum/reagents` holder
+- `inverse_chem_val` if a reagent's purity is below this value it will 100% convert into `inverse_chem`. If above it will split into `impure_chem`. See the note on purity effects above
+- `inverse_chem` is the datum type that is created provided that it's `creation_purity` is below the `inverse_chem_val`. When the reagent is consumed it will 100% convert into this OR if the associated `datum/chemical_recipe` has a REACTION_CLEAR_INVERSE flag it will 100% convert at the end of the reaction in the `datum/reagents` holder
+- `failed_chem` is the chem that the product is 100% converted into if the purity is below the associated `datum/chemical_recipies`' `PurityMin` AT THE END OF A REACTION.
+
+When writing any reagent code ALWAYS use creation_purity. Purity is kept for internal mechanics only and won’t reflect the purity on creation.
+
+See above for purity mechanics, but this is where you set the reagents that are created. If you’re making an impure reagent I recommend looking at impure_reagents.dm to see how they’re set up and consider using the `datum/reagents/impure` as a parent.
+
+The flags you can set for `var/chemical_flags` are:
+```dm
+#define REAGENT_DEAD_PROCESS (1<<0) //allows on_mob_dead() if present in a dead body
+#define REAGENT_DONOTSPLIT (1<<1) //Do not split the chem at all during processing - ignores all purity effects
+#define REAGENT_INVISIBLE (1<<2) //Doesn't appear on handheld health analyzers.
+#define REAGENT_SNEAKYNAME (1<<3) //When inverted, the inverted chem uses the name of the original chem
+#define REAGENT_SPLITRETAINVOL (1<<4) //Retains initial volume of chem when splitting for purity effects
+
+/datum/reagent
+ var/chemical_flags
+```
+
+While you might think reagent_flags is a more sensible name - it is already used for beakers. Hopefully this doesn't trip anyone up.
+
+# Relivant vars from the holder.dm / reagents datum
+
+There are a few variables that are useful to know about
+```dm
+/datum/reagents
+ /// Current temp of the holder volume
+ var/chem_temp = 150
+ ///pH of the whole system
+ var/ph = CHEMICAL_NORMAL_PH //aka 7
+ ///cached list of reagents
+ var/list/datum/reagent/previous_reagent_list = new/list()
+ ///Hard check to see if the reagents is presently reacting
+ var/is_reacting = FALSE
+```
+- chem_temp is the temperature used in the `datum/chemical_recipe`
+- pH is a result of the sum of all reagents, as well as any changes from buffers and reactions. This is the pH used in `datum/chemical_recipe`.
+- isReacting is a bool that can be used outside to ensure that you don't touch a reagents that is reacting.
+- previous_reagent_list is a list of the previous reagents (just the typepaths, not the objects) that was present on the last handle_reactions() method. This is to prevent pointless method calls.
\ No newline at end of file
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 63ad0c0658c0d0..1c9958fa1ae79a 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -1,6 +1,4 @@
-#define CHEMICAL_QUANTISATION_LEVEL 0.0001 //stops floating point errors causing issues with checking reagent amounts
-
-
+/////////////These are used in the reagents subsystem init() and the reagent_id_typos.dm////////
/proc/build_chemical_reagent_list()
//Chemical Reagents - Initialises all /datum/reagent into a list indexed by reagent id
@@ -44,7 +42,7 @@
GLOB.chemical_reactions_list[id] += D
break // Don't bother adding ourselves to other reagent ids, it is redundant
-///////////////////////////////////////////////////////////////////////////////////
+///////////////////////////////Main reagents code/////////////////////////////////////////////
/// Holder for a bunch of [/datum/reagent]
/datum/reagents
@@ -58,6 +56,8 @@
var/atom/my_atom = null
/// Current temp of the holder volume
var/chem_temp = 150
+ ///pH of the whole system
+ var/ph = CHEMICAL_NORMAL_PH
/// unused
var/last_tick = 1
/// see [/datum/reagents/proc/metabolize] for usage
@@ -66,30 +66,34 @@
var/list/datum/reagent/addiction_list
/// various flags, see code\__DEFINES\reagents.dm
var/flags
+ ///list of reactions currently on going, this is a lazylist for optimisation
+ var/list/datum/equilibrium/reaction_list
+ ///cached list of reagents typepaths (not object references), this is a lazylist for optimisation
+ var/list/datum/reagent/previous_reagent_list
+ ///If a reaction fails due to temperature or pH, this tracks the required temperature or pH for it to be enabled.
+ var/list/failed_but_capable_reactions
+ ///Hard check to see if the reagents is presently reacting
+ var/is_reacting = FALSE
/datum/reagents/New(maximum=100, new_flags=0)
maximum_volume = maximum
-
- //I dislike having these here but map-objects are initialised before world/New() is called. >_>
- if(!GLOB.chemical_reagents_list)
- build_chemical_reagent_list()
- if(!GLOB.chemical_reactions_list)
- build_chemical_reactions_list()
-
flags = new_flags
/datum/reagents/Destroy()
- . = ..()
//We're about to delete all reagents, so lets cleanup
addiction_list = null
for(var/reagent in reagent_list)
var/datum/reagent/R = reagent
qdel(R)
reagent_list = null
+ if(is_reacting) //If false, reaction list should be cleaned up
+ force_stop_reacting()
+ QDEL_LAZYLIST(reaction_list)
+ previous_reagent_list = null
if(my_atom && my_atom.reagents == src)
my_atom.reagents = null
my_atom = null
-
+ return ..()
/**
* Adds a reagent to this holder
@@ -100,19 +104,29 @@
* * list/data - Any reagent data for this reagent, used for transferring data with reagents
* * reagtemp - Temperature of this reagent, will be equalized
* * no_react - prevents reactions being triggered by this addition
+ * * added_purity - override to force a purity when added
+ * * added_ph - override to force a pH when added
+ * * override_base_ph - ingore the present pH of the reagent, and instead use the default (i.e. if buffers/reactions alter it)
*/
-/datum/reagents/proc/add_reagent(reagent, amount, list/data=null, reagtemp = 300, no_react = 0)
+/datum/reagents/proc/add_reagent(reagent, amount, list/data=null, reagtemp = 300, added_purity = null, added_ph, no_react = 0, override_base_ph = FALSE)
if(!isnum(amount) || !amount)
return FALSE
- if(amount <= 0)
+ if(amount <= CHEMICAL_QUANTISATION_LEVEL)//To prevent small amount problems.
return FALSE
var/datum/reagent/glob_reagent = GLOB.chemical_reagents_list[reagent]
if(!glob_reagent)
- WARNING("[my_atom] attempted to add a reagent called '[reagent]' which doesn't exist. ([usr])")
+ stack_trace("[my_atom] attempted to add a reagent called '[reagent]' which doesn't exist. ([usr])")
return FALSE
+ var/datum/reagent/D = GLOB.chemical_reagents_list[reagent]
+ if(isnull(added_purity)) //Because purity additions can be 0
+ added_purity = D.creation_purity //Usually 1
+
+ if(!added_ph)
+ added_ph = D.ph
+
update_total()
var/cached_total = total_volume
if(cached_total + amount > maximum_volume)
@@ -134,7 +148,12 @@
for(var/r in cached_reagents)
var/datum/reagent/iter_reagent = r
if (iter_reagent.type == reagent)
- iter_reagent.volume += amount
+ if(override_base_ph)
+ added_ph = iter_reagent.ph
+ iter_reagent.purity = ((iter_reagent.creation_purity * iter_reagent.volume) + (added_purity * amount)) /(iter_reagent.volume + amount) //This should add the purity to the product
+ iter_reagent.creation_purity = iter_reagent.purity
+ iter_reagent.ph = ((iter_reagent.ph*(iter_reagent.volume))+(added_ph*amount))/(iter_reagent.volume+amount)
+ iter_reagent.volume += round(amount, CHEMICAL_QUANTISATION_LEVEL)
update_total()
iter_reagent.on_merge(data, amount)
@@ -146,7 +165,7 @@
set_temperature(reagtemp)
SEND_SIGNAL(src, COMSIG_REAGENTS_ADD_REAGENT, iter_reagent, amount, reagtemp, data, no_react)
- if(!no_react)
+ if(!no_react && !is_reacting) //To reduce the amount of calculations for a reaction the reaction list is only updated on a reagents addition.
handle_reactions()
return TRUE
@@ -155,12 +174,15 @@
cached_reagents += new_reagent
new_reagent.holder = src
new_reagent.volume = amount
+ new_reagent.purity = added_purity
+ new_reagent.creation_purity = added_purity
+ new_reagent.ph = added_ph
if(data)
new_reagent.data = data
new_reagent.on_new(data)
if(isliving(my_atom))
- new_reagent.on_mob_add(my_atom) //Must occur before it could posibly run on_mob_delete
+ new_reagent.on_mob_add(my_atom, amount) //Must occur before it could posibly run on_mob_delete
update_total()
if(reagtemp != cached_temp)
@@ -183,7 +205,7 @@
/// Remove a specific reagent
-/datum/reagents/proc/remove_reagent(reagent, amount, safety)//Added a safety check for the trans_id_to
+/datum/reagents/proc/remove_reagent(reagent, amount, safety = TRUE)//Added a safety check for the trans_id_to
if(isnull(amount))
amount = 0
CRASH("null amount passed to reagent code")
@@ -203,12 +225,11 @@
amount = clamp(amount, 0, R.volume)
R.volume -= amount
update_total()
- SEND_SIGNAL(src, COMSIG_REAGENTS_REM_REAGENT, QDELING(R) ? reagent : R, amount)
if(!safety)//So it does not handle reactions when it need not to
handle_reactions()
+ SEND_SIGNAL(src, COMSIG_REAGENTS_REM_REAGENT, QDELING(R) ? reagent : R, amount)
return TRUE
-
return FALSE
/// Remove an amount of reagents without caring about what they are
@@ -250,7 +271,7 @@
var/datum/reagent/R = reagent
remove_reagent(R.type, R.volume * part)
- update_total()
+ //finish_reacting() //A just in case - update total is in here - should be unneeded, make sure to test this
handle_reactions()
return amount
@@ -293,11 +314,19 @@
//Clear from relevant lists
LAZYREMOVE(addiction_list, R)
reagent_list -= R
+ LAZYREMOVE(previous_reagent_list, R.type)
qdel(R)
update_total()
SEND_SIGNAL(src, COMSIG_REAGENTS_DEL_REAGENT, reagent)
return TRUE
+//Converts the creation_purity to purity
+/datum/reagents/proc/uncache_creation_purity(id)
+ var/datum/reagent/R = has_reagent(id)
+ if(!R)
+ return
+ R.purity = R.creation_purity
+
/// Remove every reagent except this one
/datum/reagents/proc/isolate_reagent(reagent)
var/list/cached_reagents = reagent_list
@@ -362,6 +391,7 @@
if(amount < 0)
return
+ var/cached_amount = amount
var/atom/target_atom
var/datum/reagents/R
if(istype(target, /datum/reagents))
@@ -382,6 +412,10 @@
R = target.reagents
target_atom = target
+ //Set up new reagents to inherit the old ongoing reactions
+ if(!no_react)
+ transfer_reactions(R)
+
amount = min(min(amount, src.total_volume), R.maximum_volume-R.total_volume)
var/trans_data = null
var/transfer_log = list()
@@ -394,7 +428,9 @@
var/transfer_amount = T.volume * part
if(preserve_data)
trans_data = copy_data(T)
- R.add_reagent(T.type, transfer_amount * multiplier, trans_data, chem_temp, no_react = 1) //we only handle reaction after every reagent has been transfered.
+ if(T.intercept_reagents_transfer(R, cached_amount))//Use input amount instead.
+ continue
+ R.add_reagent(T.type, transfer_amount * multiplier, trans_data, chem_temp, T.purity, T.ph, no_react = TRUE) //we only handle reaction after every reagent has been transfered.
if(methods)
if(istype(target_atom, /obj/item/organ))
R.expose_single(T, target, methods, part, show_message)
@@ -403,6 +439,8 @@
T.on_transfer(target_atom, methods, transfer_amount * multiplier)
remove_reagent(T.type, transfer_amount)
transfer_log[T.type] = transfer_amount
+ if(is_type_in_list(target_atom, list(/mob/living/carbon, /obj/item/organ/stomach)))
+ R.process_mob_reagent_purity(T.type, transfer_amount * multiplier, T.purity)
else
var/to_transfer = amount
for(var/reagent in cached_reagents)
@@ -416,7 +454,9 @@
var/transfer_amount = amount
if(amount > T.volume)
transfer_amount = T.volume
- R.add_reagent(T.type, transfer_amount * multiplier, trans_data, chem_temp, no_react = 1)
+ if(T.intercept_reagents_transfer(R, cached_amount))//Use input amount instead.
+ continue
+ R.add_reagent(T.type, transfer_amount * multiplier, trans_data, chem_temp, T.purity, T.ph, no_react = TRUE) //we only handle reaction after every reagent has been transfered.
to_transfer = max(to_transfer - transfer_amount , 0)
if(methods)
if(istype(target_atom, /obj/item/organ))
@@ -426,11 +466,14 @@
T.on_transfer(target_atom, methods, transfer_amount * multiplier)
remove_reagent(T.type, transfer_amount)
transfer_log[T.type] = transfer_amount
+ if(is_type_in_list(target_atom, list(/mob/living/carbon, /obj/item/organ/stomach)))
+ R.process_mob_reagent_purity(T.type, transfer_amount * multiplier, T.purity)
if(transfered_by && target_atom)
target_atom.add_hiddenprint(transfered_by) //log prints so admins can figure out who touched it last.
log_combat(transfered_by, target_atom, "transferred reagents ([log_list(transfer_log)]) from [my_atom] to")
+
update_total()
R.update_total()
if(!no_react)
@@ -447,18 +490,23 @@
return
if(amount < 0)
return
-
+
+ var/cached_amount = amount
var/datum/reagents/R = target.reagents
if(src.get_reagent_amount(reagent) 0)
- add_reagent(T.type, T.volume * change)
+ add_reagent(T.type, T.volume * change, added_purity = T.purity)
else
remove_reagent(T.type, abs(T.volume * change)) //absolute value to prevent a double negative situation (removing -50% would be adding 50%)
@@ -684,127 +736,416 @@
update_total()
/// Handle any reactions possible in this holder
+/// Also UPDATES the reaction list
+/// High potential for infinite loopsa if you're editing this.
/datum/reagents/proc/handle_reactions()
+ if(QDELING(src))
+ CRASH("[my_atom] is trying to handle reactions while being flagged for deletion. It presently has [length(reagent_list)] number of reactants in it. If that is over 0 then something terrible happened.")
+
+ if(!length(reagent_list))//The liver is calling this method a lot, and is often empty of reagents so it's pointless busywork. It should be an easy fix, but I'm nervous about touching things beyond scope. Also since everything is so handle_reactions() trigger happy it might be a good idea having this check anyways.
+ return FALSE
+
if(flags & NO_REACT)
- return 0 //Yup, no reactions here. No siree.
+ if(is_reacting)
+ force_stop_reacting() //Force anything that is trying to to stop
+ return FALSE //Yup, no reactions here. No siree.
+
+ if(is_reacting)//Prevent wasteful calculations
+ if(datum_flags != DF_ISPROCESSING)//If we're reacting - but not processing (i.e. we've transfered)
+ START_PROCESSING(SSreagents, src)
+ if(!(has_changed_state()))
+ return FALSE
var/list/cached_reagents = reagent_list
var/list/cached_reactions = GLOB.chemical_reactions_list
var/datum/cached_my_atom = my_atom
+ LAZYNULL(failed_but_capable_reactions)
. = 0
- var/reaction_occurred
- do
- var/list/possible_reactions = list()
- reaction_occurred = FALSE
- for(var/reagent in cached_reagents)
- var/datum/reagent/R = reagent
- for(var/reaction in cached_reactions[R.type]) // Was a big list but now it should be smaller since we filtered it with our reagent id
- if(!reaction)
- continue
-
- var/datum/chemical_reaction/C = reaction
- var/list/cached_required_reagents = C.required_reagents
- var/total_required_reagents = cached_required_reagents.len
- var/total_matching_reagents = 0
- var/list/cached_required_catalysts = C.required_catalysts
- var/total_required_catalysts = cached_required_catalysts.len
- var/total_matching_catalysts= 0
- var/matching_container = FALSE
- var/matching_other = FALSE
- var/required_temp = C.required_temp
- var/is_cold_recipe = C.is_cold_recipe
- var/meets_temp_requirement = FALSE
-
- for(var/B in cached_required_reagents)
- if(!has_reagent(B, cached_required_reagents[B]))
- break
- total_matching_reagents++
- for(var/B in cached_required_catalysts)
- if(!has_reagent(B, cached_required_catalysts[B]))
- break
- total_matching_catalysts++
- if(cached_my_atom)
- if(!C.required_container)
- matching_container = TRUE
- else
- if(cached_my_atom.type == C.required_container)
- matching_container = TRUE
- if (isliving(cached_my_atom) && !C.mob_react) //Makes it so certain chemical reactions don't occur in mobs
- matching_container = FALSE
- if(!C.required_other)
- matching_other = TRUE
-
- else if(istype(cached_my_atom, /obj/item/slime_extract))
- var/obj/item/slime_extract/M = cached_my_atom
+ var/list/possible_reactions = list()
+ for(var/_reagent in cached_reagents)
+ var/datum/reagent/reagent = _reagent
+ for(var/_reaction in cached_reactions[reagent.type]) // Was a big list but now it should be smaller since we filtered it with our reagent id
+ if(!_reaction)
+ continue
- if(M.Uses > 0) // added a limit to slime cores -- Muskets requested this
- matching_other = TRUE
+ var/datum/chemical_reaction/reaction = _reaction
+ if(!reaction.required_reagents)//Don't bring in empty ones
+ continue
+ var/list/cached_required_reagents = reaction.required_reagents
+ var/total_required_reagents = cached_required_reagents.len
+ var/total_matching_reagents = 0
+ var/list/cached_required_catalysts = reaction.required_catalysts
+ var/total_required_catalysts = cached_required_catalysts.len
+ var/total_matching_catalysts= 0
+ var/matching_container = FALSE
+ var/matching_other = FALSE
+ var/required_temp = reaction.required_temp
+ var/is_cold_recipe = reaction.is_cold_recipe
+ var/meets_temp_requirement = FALSE
+ var/meets_ph_requirement = FALSE
+ var/granularity = 1
+ if(!(reaction.reaction_flags & REACTION_INSTANT))
+ granularity = 0.01
+
+ for(var/req_reagent in cached_required_reagents)
+ if(!has_reagent(req_reagent, (cached_required_reagents[req_reagent]*granularity)))
+ break
+ total_matching_reagents++
+ for(var/_catalyst in cached_required_catalysts)
+ if(!has_reagent(_catalyst, (cached_required_catalysts[_catalyst]*granularity)))
+ break
+ total_matching_catalysts++
+ if(cached_my_atom)
+ if(!reaction.required_container)
+ matching_container = TRUE
else
- if(!C.required_container)
+ if(cached_my_atom.type == reaction.required_container)
matching_container = TRUE
- if(!C.required_other)
- matching_other = TRUE
-
- if(required_temp == 0 || (is_cold_recipe && chem_temp <= required_temp) || (!is_cold_recipe && chem_temp >= required_temp))
- meets_temp_requirement = TRUE
+ if (isliving(cached_my_atom) && !reaction.mob_react) //Makes it so certain chemical reactions don't occur in mobs
+ matching_container = FALSE
+ if(!reaction.required_other)
+ matching_other = TRUE
- if(total_matching_reagents == total_required_reagents && total_matching_catalysts == total_required_catalysts && matching_container && matching_other && meets_temp_requirement)
- possible_reactions += C
+ else if(istype(cached_my_atom, /obj/item/slime_extract))
+ var/obj/item/slime_extract/extract = cached_my_atom
- if(possible_reactions.len)
- var/datum/chemical_reaction/selected_reaction = possible_reactions[1]
- //select the reaction with the most extreme temperature requirements
- for(var/V in possible_reactions)
- var/datum/chemical_reaction/competitor = V
- if(selected_reaction.is_cold_recipe) //if there are no recipe conflicts, everything in possible_reactions will have this same value for is_cold_reaction. warranty void if assumption not met.
- if(competitor.required_temp <= selected_reaction.required_temp)
- selected_reaction = competitor
+ if(extract.Uses > 0) // added a limit to slime cores -- Muskets requested this
+ matching_other = TRUE
+ else
+ if(!reaction.required_container)
+ matching_container = TRUE
+ if(!reaction.required_other)
+ matching_other = TRUE
+
+ if(required_temp == 0 || (is_cold_recipe && chem_temp <= required_temp) || (!is_cold_recipe && chem_temp >= required_temp))
+ meets_temp_requirement = TRUE
+
+ if(((ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
+ meets_ph_requirement = TRUE
+
+ if(total_matching_reagents == total_required_reagents && total_matching_catalysts == total_required_catalysts && matching_container && matching_other)
+ if(meets_temp_requirement && meets_ph_requirement)
+ possible_reactions += reaction
else
- if(competitor.required_temp >= selected_reaction.required_temp)
- selected_reaction = competitor
- var/list/cached_required_reagents = selected_reaction.required_reagents
- var/list/cached_results = selected_reaction.results
- var/list/multiplier = INFINITY
- for(var/B in cached_required_reagents)
- multiplier = min(multiplier, round(get_reagent_amount(B) / cached_required_reagents[B]))
-
- for(var/B in cached_required_reagents)
- remove_reagent(B, (multiplier * cached_required_reagents[B]), safety = 1)
-
- for(var/P in selected_reaction.results)
- multiplier = max(multiplier, 1) //this shouldn't happen ...
- SSblackbox.record_feedback("tally", "chemical_reaction", cached_results[P]*multiplier, P)
- add_reagent(P, cached_results[P]*multiplier, null, chem_temp)
-
- var/list/seen = viewers(4, get_turf(my_atom))
- var/iconhtml = icon2html(cached_my_atom, seen)
- if(cached_my_atom)
- if(!ismob(cached_my_atom)) // No bubbling mobs
- if(selected_reaction.mix_sound)
- playsound(get_turf(cached_my_atom), selected_reaction.mix_sound, 80, TRUE)
-
- for(var/mob/M in seen)
- to_chat(M, "[iconhtml] [selected_reaction.mix_message]")
-
- if(istype(cached_my_atom, /obj/item/slime_extract))
- var/obj/item/slime_extract/ME2 = my_atom
- ME2.Uses--
- if(ME2.Uses <= 0) // give the notification that the slime core is dead
- for(var/mob/M in seen)
- to_chat(M, "[iconhtml] \The [my_atom]'s power is consumed in the reaction.")
- ME2.name = "used slime extract"
- ME2.desc = "This extract has been used up."
-
- selected_reaction.on_reaction(src, multiplier)
- reaction_occurred = TRUE
+ LAZYADD(failed_but_capable_reactions, reaction)
+
+ update_previous_reagent_list()
+ //This is the point where we have all the possible reactions from a reagent/catalyst point of view, so we set up the reaction list
+ for(var/_possible_reaction in possible_reactions)
+ var/datum/chemical_reaction/selected_reaction = _possible_reaction
+ if((selected_reaction.reaction_flags & REACTION_INSTANT) || (flags & REAGENT_HOLDER_INSTANT_REACT)) //If we have instant reactions, we process them here
+ instant_react(selected_reaction)
.++
+ update_total()
+ continue
+ else
+ var/exists = FALSE
+ for(var/_equilibrium in reaction_list)
+ var/datum/equilibrium/E_exist = _equilibrium
+ if(ispath(E_exist.reaction.type, selected_reaction.type)) //Don't add duplicates
+ exists = TRUE
+
+ //Add it if it doesn't exist in the list
+ if(!exists)
+ is_reacting = TRUE//Prevent any on_reaction() procs from infinite looping
+ var/datum/equilibrium/equilibrium = new (selected_reaction, src) //Otherwise we add them to the processing list.
+ if(equilibrium.to_delete)//failed startup checks
+ qdel(equilibrium)
+ else
+ //Adding is done in new(), deletion is in qdel
+ equilibrium.reaction.on_reaction(equilibrium, src, equilibrium.multiplier)
+ equilibrium.react_timestep(1)//Get an initial step going so there's not a delay between setup and start - DO NOT ADD THIS TO equilibrium.NEW()
+
+ if(LAZYLEN(reaction_list))
+ is_reacting = TRUE //We've entered the reaction phase - this is set here so any reagent handling called in on_reaction() doesn't cause infinite loops
+ START_PROCESSING(SSreagents, src) //see process() to see how reactions are handled
+ else
+ is_reacting = FALSE
- while(reaction_occurred)
- update_total()
if(.)
SEND_SIGNAL(src, COMSIG_REAGENTS_REACTED, .)
+/*
+* Main Reaction loop handler, Do not call this directly
+*
+* Checks to see if there's a reaction, then processes over the reaction list, removing them if flagged
+* If any are ended, it displays the reaction message and removes it from the reaction list
+* If the list is empty at the end it finishes reacting.
+* Arguments:
+* * delta_time - the time between each time step
+*/
+/datum/reagents/process(delta_time)
+ if(!is_reacting)
+ force_stop_reacting()
+ stack_trace("[src] | [my_atom] was forced to stop reacting. This might be unintentional.")
+ //sum of output messages.
+ var/list/mix_message = list()
+ //Process over our reaction list
+ //See equilibrium.dm for mechanics
+ var/num_reactions = 0
+ for(var/_equilibrium in reaction_list)
+ var/datum/equilibrium/equilibrium = _equilibrium
+ //Continue reacting
+ equilibrium.react_timestep(delta_time)
+ num_reactions++
+ //if it's been flagged to delete
+ if(equilibrium.to_delete)
+ var/temp_mix_message = end_reaction(equilibrium)
+ if(!text_in_list(temp_mix_message, mix_message))
+ mix_message += temp_mix_message
+ continue
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[equilibrium.reaction.type] total reaction steps")
+
+ if(num_reactions)
+ SEND_SIGNAL(src, COMSIG_REAGENTS_REACTION_STEP, num_reactions, delta_time)
+
+ if(length(mix_message)) //This is only at the end
+ my_atom.audible_message("[icon2html(my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [mix_message.Join()]")
+
+ if(!LAZYLEN(reaction_list))
+ finish_reacting()
+ else
+ update_total()
+ handle_reactions()
+
+/*
+* This ends a single instance of an ongoing reaction
+*
+* Arguments:
+* * E - the equilibrium that will be ended
+* Returns:
+* * mix_message - the associated mix message of a reaction
+*/
+/datum/reagents/proc/end_reaction(datum/equilibrium/equilibrium)
+ if(!equilibrium.holder || !equilibrium.reaction) //Somehow I'm getting empty equilibrium. This is here to handle them
+ LAZYREMOVE(reaction_list, equilibrium)
+ qdel(equilibrium)
+ stack_trace("The equilibrium datum currently processing in this reagents datum had a nulled holder or nulled reaction. src holder:[my_atom] || src type:[my_atom.type] ") //Shouldn't happen. Does happen
+ return
+ if(equilibrium.holder != src) //When called from Destroy() eqs are nulled in smoke. This is very strange. This is probably causing it to spam smoke because of the runtime interupting the removal.
+ stack_trace("The equilibrium datum currently processing in this reagents datum had a desynced holder to the ending reaction. src holder:[my_atom] | equilibrium holder:[equilibrium.holder.my_atom] || src type:[my_atom.type] | equilibrium holder:[equilibrium.holder.my_atom.type]")
+ LAZYREMOVE(reaction_list, equilibrium)
+ equilibrium.reaction.reaction_finish(src, equilibrium.reacted_vol)
+ var/reaction_message = equilibrium.reaction.mix_message
+ if(equilibrium.reaction.mix_sound)
+ playsound(get_turf(my_atom), equilibrium.reaction.mix_sound, 80, TRUE)
+ qdel(equilibrium)
+ update_total()
+ SEND_SIGNAL(src, COMSIG_REAGENTS_REACTED, .)
+ return reaction_message
+
+/*
+* This stops the holder from processing at the end of a series of reactions (i.e. when all the equilibriums are completed)
+*
+* Also resets reaction variables to be null/empty/FALSE so that it can restart correctly in the future
+*/
+/datum/reagents/proc/finish_reacting()
+ STOP_PROCESSING(SSreagents, src)
+ is_reacting = FALSE
+ //Cap off values
+ for(var/_reagent in reagent_list)
+ var/datum/reagent/reagent = _reagent
+ reagent.volume = round(reagent.volume, CHEMICAL_VOLUME_ROUNDING)//To prevent runaways.
+ LAZYNULL(previous_reagent_list) //reset it to 0 - because any change will be different now.
+ update_total()
+ if(!QDELING(src))
+ handle_reactions() //Should be okay without. Each step checks.
+
+/*
+* Force stops the current holder/reagents datum from reacting
+*
+* Calls end_reaction() for each equlilbrium datum in reaction_list and finish_reacting()
+* Usually only called when a datum is transfered into a NO_REACT container
+*/
+/datum/reagents/proc/force_stop_reacting()
+ var/list/mix_message = list()
+ for(var/_equilibrium in reaction_list)
+ var/datum/equilibrium/equilibrium = _equilibrium
+ mix_message += end_reaction(equilibrium)
+ if(length(mix_message))
+ my_atom.audible_message("[icon2html(my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [mix_message.Join()]")
+ finish_reacting()
+
+/*
+* Force stops a specific reagent's associated reaction if it exists
+*
+* Mostly used if a reagent is being taken out by trans_id_to
+* Might have some other applciations
+* Returns TRUE if it stopped something, FALSE if it didn't
+* Arguments:
+* * reagent - the reagent PRODUCT that we're seeking reactions for, any and all found will be shut down
+*/
+/datum/reagents/proc/force_stop_reagent_reacting(datum/reagent/reagent)
+ var/any_stopped = FALSE
+ var/list/mix_message = list()
+ for(var/_equilibrium in reaction_list)
+ var/datum/equilibrium/equilibrium = _equilibrium
+ for(var/result in equilibrium.reaction.results)
+ if(result == reagent.type)
+ mix_message += end_reaction(equilibrium)
+ any_stopped = TRUE
+ if(length(mix_message))
+ my_atom.audible_message("[icon2html(my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [mix_message.Join()]")
+ return any_stopped
+
+/*
+* Transfers the reaction_list to a new reagents datum
+*
+* Arguments:
+* * target - the datum/reagents that this src is being transfered into
+*/
+/datum/reagents/proc/transfer_reactions(datum/reagents/target)
+ if(QDELETED(target))
+ CRASH("transfer_reactions() had a [target] ([target.type]) passed to it when it was set to qdel, or it isn't a reagents datum.")
+ if(!reaction_list)
+ return
+ for(var/reaction in reaction_list)
+ var/datum/equilibrium/reaction_source = reaction
+ var/exists = FALSE
+ for(var/reaction2 in target.reaction_list) //Don't add duplicates
+ var/datum/equilibrium/reaction_target = reaction2
+ if(reaction_source.reaction.type == reaction_target.reaction.type)
+ exists = TRUE
+ if(exists)
+ continue
+ if(!reaction_source.holder)
+ CRASH("reaction_source is missing a holder in transfer_reactions()!")
+
+ var/datum/equilibrium/new_E = new (reaction_source.reaction, target)//addition to reaction_list is done in new()
+ if(new_E.to_delete)//failed startup checks
+ qdel(new_E)
+
+ target.previous_reagent_list = LAZYLISTDUPLICATE(previous_reagent_list)
+ target.is_reacting = is_reacting
+
+
+///Checks to see if the reagents has a difference in reagents_list and previous_reagent_list (I.e. if there's a difference between the previous call and the last)
+///Also checks to see if the saved reactions in failed_but_capable_reactions can start as a result of temp/pH change
+/datum/reagents/proc/has_changed_state()
+ //Check if reagents are different
+ var/total_matching_reagents = 0
+ for(var/reagent in previous_reagent_list)
+ if(has_reagent(reagent))
+ total_matching_reagents++
+ if(total_matching_reagents != reagent_list.len)
+ return TRUE
+
+ //Check our last reactions
+ for(var/_reaction in failed_but_capable_reactions)
+ var/datum/chemical_reaction/reaction = _reaction
+ if(reaction.is_cold_recipe)
+ if(reaction.required_temp < chem_temp)
+ return TRUE
+ else
+ if(reaction.required_temp < chem_temp)
+ return TRUE
+ if(((ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
+ return TRUE
+ return FALSE
+
+/datum/reagents/proc/update_previous_reagent_list()
+ LAZYNULL(previous_reagent_list)
+ for(var/_reagent in reagent_list)
+ var/datum/reagent/reagent = _reagent
+ LAZYADD(previous_reagent_list, reagent.type)
+
+///Old reaction mechanics, edited to work on one only
+///This is changed from the old - purity of the reagents will affect yield
+/datum/reagents/proc/instant_react(datum/chemical_reaction/selected_reaction)
+ var/list/cached_required_reagents = selected_reaction.required_reagents
+ var/list/cached_results = selected_reaction.results
+ var/datum/cached_my_atom = my_atom
+ var/multiplier = INFINITY
+ for(var/reagent in cached_required_reagents)
+ multiplier = min(multiplier, round(get_reagent_amount(reagent) / cached_required_reagents[reagent]))
+
+ if(multiplier == 0)//Incase we're missing reagents - usually from on_reaction being called in an equlibrium when the results.len == 0 handlier catches a misflagged reaction
+ return FALSE
+ var/sum_purity = 0
+ for(var/_reagent in cached_required_reagents)
+ var/datum/reagent/reagent = has_reagent(_reagent)
+ sum_purity += reagent.purity
+ remove_reagent(_reagent, (multiplier * cached_required_reagents[_reagent]), safety = 1)
+ sum_purity /= cached_required_reagents.len
+
+ for(var/product in selected_reaction.results)
+ multiplier = max(multiplier, 1) //this shouldn't happen ...
+ var/yield = (cached_results[product]*multiplier)*sum_purity
+ SSblackbox.record_feedback("tally", "chemical_reaction", yield, product)
+ add_reagent(product, yield, null, chem_temp, sum_purity)
+
+ var/list/seen = viewers(4, get_turf(my_atom))
+ var/iconhtml = icon2html(cached_my_atom, seen)
+ if(cached_my_atom)
+ if(!ismob(cached_my_atom)) // No bubbling mobs
+ if(selected_reaction.mix_sound)
+ playsound(get_turf(cached_my_atom), selected_reaction.mix_sound, 80, TRUE)
+
+ my_atom.audible_message("[iconhtml] [selected_reaction.mix_message]")
+
+ if(istype(cached_my_atom, /obj/item/slime_extract))
+ var/obj/item/slime_extract/extract = my_atom
+ extract.Uses--
+ if(extract.Uses <= 0) // give the notification that the slime core is dead
+ my_atom.visible_message("[iconhtml] \The [my_atom]'s power is consumed in the reaction.")
+ extract.name = "used slime extract"
+ extract.desc = "This extract has been used up."
+
+ selected_reaction.on_reaction(null, src, multiplier)
+
+///Possibly remove - see if multiple instant reactions is okay (Though, this "sorts" reactions by temp decending)
+///Presently unused
+/datum/reagents/proc/get_priority_instant_reaction(list/possible_reactions)
+ if(!length(possible_reactions))
+ return FALSE
+ var/datum/chemical_reaction/selected_reaction = possible_reactions[1]
+ //select the reaction with the most extreme temperature requirements
+ for(var/_reaction in possible_reactions)
+ var/datum/chemical_reaction/competitor = _reaction
+ if(selected_reaction.is_cold_recipe)
+ if(competitor.required_temp <= selected_reaction.required_temp)
+ selected_reaction = competitor
+ else
+ if(competitor.required_temp >= selected_reaction.required_temp)
+ selected_reaction = competitor
+ return selected_reaction
+
+/*Processes the reagents in the holder and converts them, only called in a mob/living/carbon on addition
+*
+* Arguments:
+* * reagent - the added reagent datum/object
+* * added_volume - the volume of the reagent that was added (since it can already exist in a mob)
+* * added_purity - the purity of the added volume
+*/
+/datum/reagents/proc/process_mob_reagent_purity(_reagent, added_volume, added_purity)
+ var/datum/reagent/R = has_reagent(_reagent)
+ if(!R)
+ stack_trace("Tried to process reagent purity for [_reagent], but 0 volume was found right after it was added!") //This can happen from smoking, where the volume is 0 after adding?
+ return
+ if (R.purity == 1)
+ return
+ if(R.chemical_flags & REAGENT_DONOTSPLIT)
+ R.purity = 1
+ return
+ if(R.purity < 0)
+ stack_trace("Purity below 0 for chem: [type]!")
+ R.purity = 0
+
+ if ((R.inverse_chem_val > R.purity) && (R.inverse_chem))//Turns all of a added reagent into the inverse chem
+ remove_reagent(R.type, added_volume, FALSE)
+ add_reagent(R.inverse_chem, added_volume, FALSE, added_purity = 1-R.creation_purity)
+ var/datum/reagent/inverse_reagent = has_reagent(R.inverse_chem)
+ if(inverse_reagent.chemical_flags & REAGENT_SNEAKYNAME)
+ inverse_reagent.name = R.name//Negative effects are hidden
+ if(inverse_reagent.chemical_flags & REAGENT_INVISIBLE)
+ inverse_reagent.chemical_flags |= (REAGENT_INVISIBLE)
+ else if (R.impure_chem)
+ var/impureVol = added_volume * (1 - R.purity) //turns impure ratio into impure chem
+ if(!(R.chemical_flags & REAGENT_SPLITRETAINVOL))
+ remove_reagent(R.type, impureVol, FALSE)
+ add_reagent(R.impure_chem, impureVol, FALSE, added_purity = 1-R.creation_purity)
+ R.purity = 1 //prevent this process from repeating (this is why creation_purity exists)
/// Updates [/datum/reagents/var/total_volume]
/datum/reagents/proc/update_total()
@@ -812,10 +1153,14 @@
total_volume = 0
for(var/reagent in cached_reagents)
var/datum/reagent/R = reagent
- if(R.volume < 0.05)
+ if((R.volume < 0.05) && !is_reacting)
+ del_reagent(R.type)
+ else if(R.volume <= CHEMICAL_VOLUME_MINIMUM)//For clarity
del_reagent(R.type)
else
total_volume += R.volume
+ recalculate_sum_ph()
+
/**
* Applies the relevant expose_ proc for every reagent in this holder
@@ -873,6 +1218,15 @@
return round(R.volume, CHEMICAL_QUANTISATION_LEVEL)
return 0
+/// Get the purity of this reagent
+/datum/reagents/proc/get_reagent_purity(reagent)
+ var/list/cached_reagents = reagent_list
+ for(var/_reagent in cached_reagents)
+ var/datum/reagent/R = _reagent
+ if (R.type == reagent)
+ return round(R.purity, 0.01)
+ return 0
+
/// Get a comma separated string of every reagent name in this holder. UNUSED
/datum/reagents/proc/get_reagent_names()
var/list/names = list()
@@ -1018,6 +1372,65 @@
chem_temp = _temperature
SEND_SIGNAL(src, COMSIG_REAGENTS_TEMP_CHANGE, _temperature, .)
+/*
+* Adjusts the base pH of all of the reagents in a beaker
+*
+* - moves it towards acidic
+* + moves it towards basic
+* Arguments:
+* * value - How much to adjust the base pH by
+*/
+/datum/reagents/proc/adjust_all_reagents_ph(value, lower_limit = 0, upper_limit = 14)
+ for(var/reagent in reagent_list)
+ var/datum/reagent/R = reagent
+ R.ph = clamp(R.ph + value, lower_limit, upper_limit)
+
+/*
+* Adjusts the base pH of all of the listed types
+*
+* - moves it towards acidic
+* + moves it towards basic
+* Arguments:
+* * input_reagents_list - list of reagents to adjust
+* * value - How much to adjust the base pH by
+*/
+/datum/reagents/proc/adjust_specific_reagent_list_ph(list/input_reagents_list, value, lower_limit = 0, upper_limit = 14)
+ for(var/reagent in input_reagents_list)
+ var/datum/reagent/R = get_reagent(reagent)
+ if(!R) //We can call this with missing reagents.
+ continue
+ R.ph = clamp(R.ph + value, lower_limit, upper_limit)
+
+/*
+* Adjusts the base pH of a specific type
+*
+* - moves it towards acidic
+* + moves it towards basic
+* Arguments:
+* * input_reagent - type path of the reagent
+* * value - How much to adjust the base pH by
+* * lower_limit - how low the pH can go
+* * upper_limit - how high the pH can go
+*/
+/datum/reagents/proc/adjust_specific_reagent_ph(input_reagent, value, lower_limit = 0, upper_limit = 14)
+ var/datum/reagent/R = get_reagent(input_reagent)
+ if(!R) //We can call this with missing reagents.
+ return FALSE
+ R.ph = clamp(R.ph + value, lower_limit, upper_limit)
+
+/*
+* Updates the reagents datum pH based off the volume weighted sum of the reagent_list's reagent pH
+*/
+/datum/reagents/proc/recalculate_sum_ph()
+ if(!reagent_list || !total_volume) //Ensure that this is true
+ ph = CHEMICAL_NORMAL_PH
+ return
+ var/total_ph = 0
+ for(var/reagent in reagent_list)
+ var/datum/reagent/R = get_reagent(reagent) //we need the specific instance
+ total_ph += (R.ph * R.volume)
+ //Keep limited
+ ph = clamp(total_ph/total_volume, 0, 14)
/**
* Used in attack logs for reagents in pills and such
diff --git a/code/modules/reagents/chemistry/items.dm b/code/modules/reagents/chemistry/items.dm
new file mode 100644
index 00000000000000..d4f7bf6fdfd82f
--- /dev/null
+++ b/code/modules/reagents/chemistry/items.dm
@@ -0,0 +1,164 @@
+///if the ph_meter gives a detailed output
+#define DETAILED_CHEM_OUTPUT 1
+///if the pH meter gives a shorter output
+#define SHORTENED_CHEM_OUTPUT 0
+
+/*
+* a pH booklet that contains pH paper pages that will change color depending on the pH of the reagents datum it's attacked onto
+*/
+/obj/item/ph_booklet
+ name = "pH indicator booklet"
+ desc = "A booklet containing paper soaked in universal indicator."
+ icon_state = "pHbooklet"
+ icon = 'icons/obj/chemical.dmi'
+ item_flags = NOBLUDGEON
+ resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_TINY
+ ///How many pages the booklet holds
+ var/number_of_pages = 50
+
+//A little janky with pockets
+/obj/item/ph_booklet/attack_hand(mob/user)
+ if(user.get_held_index_of_item(src))//Does this check pockets too..?
+ if(number_of_pages == 50)
+ icon_state = "pHbooklet_open"
+ if(!number_of_pages)
+ to_chat(user, "[src] is empty!")
+ add_fingerprint(user)
+ return
+ var/obj/item/ph_paper/page = new(get_turf(user))
+ page.add_fingerprint(user)
+ user.put_in_active_hand(page)
+ to_chat(user, "You take [page] out of \the [src].")
+ number_of_pages--
+ playsound(user.loc, 'sound/items/poster_ripped.ogg', 50, TRUE)
+ add_fingerprint(user)
+ if(!number_of_pages)
+ icon_state = "pHbooklet_empty"
+ return
+ var/I = user.get_active_held_item()
+ if(!I)
+ user.put_in_active_hand(src)
+ return ..()
+
+/obj/item/ph_booklet/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params)
+ var/mob/living/user = usr
+ if(!isliving(user))
+ return
+ if(HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
+ return
+ if(!number_of_pages)
+ to_chat(user, "[src] is empty!")
+ add_fingerprint(user)
+ return
+ if(number_of_pages == 50)
+ icon_state = "pHbooklet_open"
+ var/obj/item/ph_paper/P = new(get_turf(user))
+ P.add_fingerprint(user)
+ user.put_in_active_hand(P)
+ to_chat(user, "You take [P] out of \the [src].")
+ number_of_pages--
+ playsound(user.loc, 'sound/items/poster_ripped.ogg', 50, TRUE)
+ add_fingerprint(user)
+ if(!number_of_pages)
+ icon_state = "pHbookletEmpty"
+
+/*
+* pH paper will change color depending on the pH of the reagents datum it's attacked onto
+*/
+/obj/item/ph_paper
+ name = "pH indicator strip"
+ desc = "A piece of paper that will change colour depending on the pH of a solution."
+ icon_state = "pHpaper"
+ icon = 'icons/obj/chemical.dmi'
+ item_flags = NOBLUDGEON
+ color = "#f5c352"
+ resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_TINY
+ ///If the paper was used, and therefore cannot change color again
+ var/used = FALSE
+
+/obj/item/ph_paper/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
+ var/obj/item/reagent_containers/cont = target
+ if(!istype(cont))
+ return
+ if(used == TRUE)
+ to_chat(user, "[src] has already been used!")
+ return
+ if(!LAZYLEN(cont.reagents.reagent_list))
+ return
+ switch(round(cont.reagents.ph, 1))
+ if(14 to INFINITY)
+ color = "#462c83"
+ if(13 to 14)
+ color = "#63459b"
+ if(12 to 13)
+ color = "#5a51a2"
+ if(11 to 12)
+ color = "#3853a4"
+ if(10 to 11)
+ color = "#3f93cf"
+ if(9 to 10)
+ color = "#0bb9b7"
+ if(8 to 9)
+ color = "#23b36e"
+ if(7 to 8)
+ color = "#3aa651"
+ if(6 to 7)
+ color = "#4cb849"
+ if(5 to 6)
+ color = "#b5d335"
+ if(4 to 5)
+ color = "#f7ec1e"
+ if(3 to 4)
+ color = "#fbc314"
+ if(2 to 3)
+ color = "#f26724"
+ if(1 to 2)
+ color = "#ef1d26"
+ if(-INFINITY to 1)
+ color = "#c6040c"
+ desc += " The paper looks to be around a pH of [round(cont.reagents.ph, 1)]"
+ name = "used [name]"
+ used = TRUE
+
+/*
+* pH meter that will give a detailed or truncated analysis of all the reagents in of an object with a reagents datum attached to it. Only way of detecting purity for now.
+*/
+/obj/item/ph_meter
+ name = "Chemistry Analyser"
+ desc = "An electrode attached to a small circuit box that will display details of a solution. Can be toggled to provide a description of each of the reagents. The screen currently displays nothing."
+ icon_state = "pHmeter"
+ icon = 'icons/obj/chemical.dmi'
+ resistance_flags = FLAMMABLE
+ w_class = WEIGHT_CLASS_TINY
+ ///level of detail for output for the meter
+ var/scanmode = DETAILED_CHEM_OUTPUT
+
+/obj/item/ph_meter/attack_self(mob/user)
+ if(scanmode == SHORTENED_CHEM_OUTPUT)
+ to_chat(user, "You switch the chemical analyzer to provide a detailed description of each reagent.")
+ scanmode = DETAILED_CHEM_OUTPUT
+ else
+ to_chat(user, "You switch the chemical analyzer to not include reagent descriptions in it's report.")
+ scanmode = SHORTENED_CHEM_OUTPUT
+
+/obj/item/ph_meter/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!istype(target, /obj/item/reagent_containers))
+ return
+ var/obj/item/reagent_containers/cont = target
+ if(LAZYLEN(cont.reagents.reagent_list) == null)
+ return
+ var/list/out_message = list()
+ to_chat(user, "The chemistry meter beeps and displays:")
+ out_message += "Total volume: [round(cont.volume, 0.01)] Current temperature: [round(cont.reagents.chem_temp, 0.1)]K Total pH: [round(cont.reagents.ph, 0.01)]\n"
+ out_message += "Chemicals found in the beaker:\n"
+ if(cont.reagents.is_reacting)
+ out_message += "A reaction appears to be occuring currently.\n"
+ for(var/datum/reagent/R in cont.reagents.reagent_list)
+ out_message += "[round(R.volume, 0.01)]u of [R.name], Purity: [round(R.purity, 0.01)], [(scanmode?"[(R.overdose_threshold?"Overdose: [R.overdose_threshold]u, ":"")][(R.addiction_threshold?"Addiction: [R.addiction_threshold]u, ":"")]Base pH: [initial(R.ph)], Current pH: [R.ph].":"Current pH: [R.ph].")]\n"
+ if(scanmode)
+ out_message += "Analysis: [R.description]\n"
+ to_chat(user, "[out_message.Join()]")
+ desc = "An electrode attached to a small circuit box that will display details of a solution. Can be toggled to provide a description of each of the reagents. The screen currently displays detected vol: [round(cont.volume, 0.01)] detected pH:[round(cont.reagents.ph, 0.1)]."
diff --git a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
index c87a79cb4c7513..7400ac071d5cfd 100644
--- a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm
@@ -29,6 +29,8 @@
var/amount = 30
var/recharge_amount = 10
var/recharge_counter = 0
+ ///If the UI has the pH meter shown
+ var/show_ph = TRUE
var/mutable_appearance/beaker_overlay
var/working_state = "dispenser_working"
var/nopower_state = "dispenser_nopower"
@@ -191,23 +193,26 @@
data["energy"] = cell.charge ? cell.charge * powerefficiency : "0" //To prevent NaN in the UI.
data["maxEnergy"] = cell.maxcharge * powerefficiency
data["isBeakerLoaded"] = beaker ? 1 : 0
+ data["showpH"] = show_ph
var/beakerContents[0]
var/beakerCurrentVolume = 0
if(beaker && beaker.reagents && beaker.reagents.reagent_list.len)
for(var/datum/reagent/R in beaker.reagents.reagent_list)
- beakerContents.Add(list(list("name" = R.name, "volume" = R.volume))) // list in a list because Byond merges the first list...
+ beakerContents.Add(list(list("name" = R.name, "volume" = round(R.volume, 0.01), "pH" = R.ph, "purity" = R.purity))) // list in a list because Byond merges the first list...
beakerCurrentVolume += R.volume
data["beakerContents"] = beakerContents
if (beaker)
- data["beakerCurrentVolume"] = beakerCurrentVolume
+ data["beakerCurrentVolume"] = round(beakerCurrentVolume, 0.01)
data["beakerMaxVolume"] = beaker.volume
data["beakerTransferAmounts"] = beaker.possible_transfer_amounts
+ data["beakerCurrentpH"] = round(beaker.reagents.ph, 0.01)
else
data["beakerCurrentVolume"] = null
data["beakerMaxVolume"] = null
data["beakerTransferAmounts"] = null
+ data["beakerCurrentpH"] = null
var/chemicals[0]
var/is_hallucinating = FALSE
@@ -219,7 +224,7 @@
var/chemname = temp.name
if(is_hallucinating && prob(5))
chemname = "[pick_list_replacements("hallucination.json", "chemicals")]"
- chemicals.Add(list(list("title" = chemname, "id" = ckey(temp.name))))
+ chemicals.Add(list(list("title" = chemname, "id" = ckey(temp.name), "pH" = temp.ph, "pHCol" = ConvertpHToCol(temp.ph))))
data["chemicals"] = chemicals
data["recipes"] = saved_recipes
@@ -403,6 +408,32 @@
update_icon()
return TRUE
+//Converts the pH into a tgui readable color
+/obj/machinery/chem_dispenser/proc/ConvertpHToCol(pH)
+ switch(pH)
+ if(-INFINITY to 1)
+ return "red"
+ if(1 to 2)
+ return "orange"
+ if(2 to 3)
+ return "average"
+ if(3 to 4)
+ return "yellow"
+ if(4 to 5)
+ return "olive"
+ if(5 to 6)
+ return "good"
+ if(6 to 8)
+ return "green"
+ if(8 to 9.5)
+ return "teal"
+ if(9.5 to 11)
+ return "blue"
+ if(11 to 12.5)
+ return "violet"
+ if(12.5 to INFINITY)
+ return "purple"
+
/obj/machinery/chem_dispenser/on_deconstruction()
cell = null
if(beaker)
@@ -456,6 +487,7 @@
working_state = null
nopower_state = null
pass_flags = PASSTABLE
+ show_ph = FALSE
dispensable_reagents = list(
/datum/reagent/water,
/datum/reagent/consumable/ice,
diff --git a/code/modules/reagents/chemistry/machinery/chem_heater.dm b/code/modules/reagents/chemistry/machinery/chem_heater.dm
index 17d3a56fed7b51..b0de5e7e9ee5d5 100644
--- a/code/modules/reagents/chemistry/machinery/chem_heater.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_heater.dm
@@ -1,5 +1,17 @@
+#define ENABLE_FLASHING -1
+
+///Tutorial states
+#define TUT_NO_BUFFER 50
+#define TUT_START 1
+#define TUT_HAS_REAGENTS 2
+#define TUT_IS_ACTIVE 3
+#define TUT_IS_REACTING 4
+#define TUT_FAIL 4.5
+#define TUT_COMPLETE 5
+#define TUT_MISSING 10
+
/obj/machinery/chem_heater
- name = "chemical heater"
+ name = "reaction chamber" //Maybe this name is more accurate?
density = TRUE
icon = 'icons/obj/chemical.dmi'
icon_state = "mixer0b"
@@ -12,11 +24,29 @@
var/target_temperature = 300
var/heater_coefficient = 0.05
var/on = FALSE
+ var/dispense_volume = 1
+
+ //The list of active clients using this heater, so that we can update the UI on a reaction_step. I assume there are multiple clients possible.
+ var/list/ui_client_list
+ ///If the user has the tutorial enabled
+ var/tutorial_active = FALSE
+ ///What state we're at in the tutorial
+ var/tutorial_state = 0
+/obj/machinery/chem_heater/Initialize()
+ . = ..()
+ create_reagents(200, NO_REACT)//Lets save some calculations here
+ reagents.add_reagent(/datum/reagent/reaction_agent/basic_buffer, 20)
+ reagents.add_reagent(/datum/reagent/reaction_agent/acidic_buffer, 20)
+ //TODO: comsig reaction_start and reaction_end to enable/disable the UI autoupdater - this doesn't work presently as there's a hard divide between instant and processed reactions
+
/obj/machinery/chem_heater/Destroy()
- QDEL_NULL(beaker)
+ if(beaker)
+ UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
+ QDEL_NULL(beaker)
return ..()
+
/obj/machinery/chem_heater/handle_atom_del(atom/A)
. = ..()
if(A == beaker)
@@ -40,9 +70,11 @@
return FALSE
if(beaker)
try_put_in_hand(beaker, user)
+ UnregisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP)
beaker = null
if(new_beaker)
beaker = new_beaker
+ RegisterSignal(beaker.reagents, COMSIG_REAGENTS_REACTION_STEP, .proc/on_reaction_step)
update_icon()
return TRUE
@@ -58,14 +90,60 @@
/obj/machinery/chem_heater/process(delta_time)
..()
+ //Tutorial logics
+ if(tutorial_active)
+ switch(tutorial_state)
+ if(TUT_NO_BUFFER)
+ if(reagents.has_reagent(/datum/reagent/reaction_agent/basic_buffer, 5) && reagents.has_reagent(/datum/reagent/reaction_agent/acidic_buffer, 5))
+ tutorial_state = TUT_START
+
+ if(TUT_START)
+ if(!reagents.has_reagent(/datum/reagent/reaction_agent/basic_buffer, 5) || !reagents.has_reagent(/datum/reagent/reaction_agent/acidic_buffer, 5))
+ tutorial_state = TUT_NO_BUFFER
+ return
+ if(beaker?.reagents.has_reagent(/datum/reagent/mercury, 10) || beaker?.reagents.has_reagent(/datum/reagent/chlorine, 10))
+ tutorial_state = TUT_HAS_REAGENTS
+
+ if(TUT_HAS_REAGENTS)
+ if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury, 9)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine, 9)))
+ tutorial_state = TUT_MISSING
+ return
+ if(beaker?.reagents.chem_temp > 374)//If they heated it up as asked
+ tutorial_state = TUT_IS_ACTIVE
+ target_temperature = 375
+ beaker.reagents.chem_temp = 375
+
+ if(TUT_IS_ACTIVE)
+ if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine))) //Slightly concerned that people might take ages to read and it'll react anyways
+ tutorial_state = TUT_MISSING
+ return
+ if(length(beaker?.reagents.reaction_list) == 1)//Only fudge numbers for our intentful reaction
+ beaker.reagents.chem_temp = 375
+
+ if(target_temperature >= 390)
+ tutorial_state = TUT_IS_REACTING
+
+ if(TUT_IS_REACTING)
+ if(!(beaker?.reagents.has_reagent(/datum/reagent/mercury)) || !(beaker?.reagents.has_reagent(/datum/reagent/chlorine)))
+ tutorial_state = TUT_COMPLETE
+
+ if(TUT_COMPLETE)
+ if(beaker?.reagents.has_reagent(/datum/reagent/consumable/failed_reaction))
+ tutorial_state = TUT_FAIL
+ return
+ if(!beaker?.reagents.has_reagent(/datum/reagent/medicine/calomel))
+ tutorial_state = TUT_MISSING
+
if(machine_stat & NOPOWER)
return
if(on)
if(beaker?.reagents.total_volume)
+ if(beaker.reagents.is_reacting)//on_reaction_step() handles this
+ return
//keep constant with the chemical acclimator please
beaker.reagents.adjust_thermal_energy((target_temperature - beaker.reagents.chem_temp) * heater_coefficient * delta_time * SPECIFIC_HEAT_DEFAULT * beaker.reagents.total_volume)
beaker.reagents.handle_reactions()
-
+
/obj/machinery/chem_heater/attackby(obj/item/I, mob/user, params)
if(default_deconstruction_screwdriver(user, "mixer0b", "mixer0b", I))
return
@@ -83,33 +161,208 @@
updateUsrDialog()
update_icon()
return
+
+ if(beaker)
+ if(istype(I, /obj/item/reagent_containers/dropper))
+ var/obj/item/reagent_containers/dropper/D = I
+ D.afterattack(beaker, user, 1)
+ return
+ if(istype(I, /obj/item/reagent_containers/syringe))
+ var/obj/item/reagent_containers/syringe/S = I
+ S.afterattack(beaker, user, 1)
+ return
+
return ..()
/obj/machinery/chem_heater/on_deconstruction()
replace_beaker()
return ..()
+///Forces a UI update every time a reaction step happens inside of the beaker it contains. This is so the UI is in sync with the reaction since it's important that the output matches the current conditions for pH adjustment and temperature.
+/obj/machinery/chem_heater/proc/on_reaction_step(datum/reagents/holder, num_reactions, delta_time)
+ SIGNAL_HANDLER
+ if(on)
+ holder.adjust_thermal_energy((target_temperature - beaker.reagents.chem_temp) * heater_coefficient * delta_time * SPECIFIC_HEAT_DEFAULT * beaker.reagents.total_volume * (rand(8,11) * 0.1))//Give it a little wiggle room since we're actively reacting
+ for(var/ui_client in ui_client_list)
+ var/datum/tgui/ui = ui_client
+ if(!ui)
+ stack_trace("Warning: UI in UI client list is missing in [src] (chem_heater)")
+ remove_ui_client_list(ui)
+ continue
+ ui.send_update()
+
/obj/machinery/chem_heater/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ChemHeater", name)
ui.open()
+ add_ui_client_list(ui)
+
+/obj/machinery/chem_heater/ui_close(mob/user)
+ for(var/ui_client in ui_client_list)
+ var/datum/tgui/ui = ui_client
+ if(ui.user == user)
+ remove_ui_client_list(ui)
+ return ..()
+
+/*
+*This adds an open ui client to the list - so that it can be force updated from reaction mechanisms.
+* After adding it to the list, it enables a signal incase the ui is deleted - which will call a method to remove it from the list
+* This is mostly to ensure we don't have defunct ui instances stored from any condition.
+*/
+/obj/machinery/chem_heater/proc/add_ui_client_list(new_ui)
+ LAZYADD(ui_client_list, new_ui)
+ RegisterSignal(new_ui, COMSIG_PARENT_QDELETING, .proc/on_ui_deletion)
+
+///This removes an open ui instance from the ui list and deregsiters the signal
+/obj/machinery/chem_heater/proc/remove_ui_client_list(old_ui)
+ UnregisterSignal(old_ui, COMSIG_PARENT_QDELETING)
+ LAZYREMOVE(ui_client_list, old_ui)
+
+///This catches a signal and uses it to delete the ui instance from the list
+/obj/machinery/chem_heater/proc/on_ui_deletion(datum/tgui/source, force)
+ SIGNAL_HANDLER
+ remove_ui_client_list(source)
-/obj/machinery/chem_heater/ui_data()
+/obj/machinery/chem_heater/ui_assets()
+ . = ..() || list()
+ . += get_asset_datum(/datum/asset/simple/tutorial_advisors)
+
+/obj/machinery/chem_heater/ui_data(mob/user)
var/data = list()
data["targetTemp"] = target_temperature
data["isActive"] = on
data["isBeakerLoaded"] = beaker ? 1 : 0
data["currentTemp"] = beaker ? beaker.reagents.chem_temp : null
- data["beakerCurrentVolume"] = beaker ? beaker.reagents.total_volume : null
+ data["beakerCurrentVolume"] = beaker ? round(beaker.reagents.total_volume, 0.01) : null
data["beakerMaxVolume"] = beaker ? beaker.volume : null
+ data["currentpH"] = beaker ? round(beaker.reagents.ph, 0.01) : null
+ var/upgrade_level = heater_coefficient*10
+ data["upgradeLevel"] = upgrade_level
+
+ var/list/beaker_contents = list()
+ for(var/r in beaker?.reagents.reagent_list)
+ var/datum/reagent/reagent = r
+ beaker_contents.len++
+ beaker_contents[length(beaker_contents)] = list("name" = reagent.name, "volume" = round(reagent.volume, 0.01))
+ data["beakerContents"] = beaker_contents
+
+ var/list/active_reactions = list()
+ var/flashing = 14 //for use with alertAfter - since there is no alertBefore, I set the after to 0 if true, or to the max value if false
+ for(var/_reaction in beaker?.reagents.reaction_list)
+ var/datum/equilibrium/equilibrium = _reaction
+ if(!length(beaker.reagents.reaction_list))//I'm not sure why when it explodes it causes the gui to fail (it's missing danger (?) )
+ stack_trace("how is this happening??")
+ continue
+ if(!equilibrium.reaction.results)//Incase of no result reactions
+ continue
+ var/_reagent = equilibrium.reaction.results[1]
+ var/datum/reagent/reagent = beaker?.reagents.get_reagent(_reagent) //Reactions are named after their primary products
+ if(!reagent)
+ continue
+ var/overheat = FALSE
+ var/danger = FALSE
+ var/purity_alert = 2 //same as flashing
+ if(reagent.purity < equilibrium.reaction.purity_min)
+ purity_alert = ENABLE_FLASHING//Because 0 is seen as null
+ danger = TRUE
+ if(!(flashing == ENABLE_FLASHING))//So that the pH meter flashes for ANY reactions out of optimal
+ if(equilibrium.reaction.optimal_ph_min > beaker?.reagents.ph || equilibrium.reaction.optimal_ph_max < beaker?.reagents.ph)
+ flashing = ENABLE_FLASHING
+ if(equilibrium.reaction.is_cold_recipe)
+ if(equilibrium.reaction.overheat_temp > beaker?.reagents.chem_temp)
+ danger = TRUE
+ overheat = TRUE
+ else
+ if(equilibrium.reaction.overheat_temp < beaker?.reagents.chem_temp)
+ danger = TRUE
+ overheat = TRUE
+ if(equilibrium.reaction.reaction_flags & REACTION_COMPETITIVE) //We have a compeitive reaction - concatenate the results for the different reactions
+ for(var/entry in active_reactions)
+ if(entry["name"] == reagent.name) //If we have multiple reaction methods for the same result - combine them
+ entry["reactedVol"] = equilibrium.reacted_vol
+ entry["targetVol"] = round(equilibrium.target_vol, 1)//Use the first result reagent to name the reaction detected
+ entry["quality"] = (entry["quality"] + equilibrium.reaction_quality) /2
+ continue
+ active_reactions.len++
+ active_reactions[length(active_reactions)] = list("name" = reagent.name, "danger" = danger, "purityAlert" = purity_alert, "quality" = equilibrium.reaction_quality, "overheat" = overheat, "inverse" = reagent.inverse_chem_val, "minPure" = equilibrium.reaction.purity_min, "reactedVol" = equilibrium.reacted_vol, "targetVol" = round(equilibrium.target_vol, 1))//Use the first result reagent to name the reaction detected
+ data["activeReactions"] = active_reactions
+ data["isFlashing"] = flashing
+
+ data["acidicBufferVol"] = reagents.get_reagent_amount(/datum/reagent/reaction_agent/acidic_buffer)
+ data["basicBufferVol"] = reagents.get_reagent_amount(/datum/reagent/reaction_agent/basic_buffer)
+ data["dispenseVolume"] = dispense_volume
+
+ data["tutorialMessage"] = null
+ //Tutorial output
+ if(tutorial_active)
+ switch(tutorial_state)
+ if(TUT_NO_BUFFER)//missing buffer
+ data["tutorialMessage"] = {"It looks like you’re a little low on buffers, here’s how to make more:
+
+Acidic buffer: 2 parts Sodium
+ 2 parts Hydrogen
+ 2 parts Ethanol
+ 2 parts Water
+
+Basic buffer: 3 parts Ammonia
+ 2 parts Chlorine
+ 2 parts Hydrogen
+ 2 parts Oxygen
+
+Heat either up to speed up the reaction.
+
+When the reactions are done, refill your chamber by pressing the Draw all buttons, to the right of the respective volume indicators.
+
+To continue with the tutorial, fill both of your acidic and alkaline volumes to at least 5u."}
+ if(TUT_START)//Default start
+ data["tutorialMessage"] = {"Hello and welcome to the exciting world of chemistry! This help option will teach you the basic of reactions by guiding you through a calomel reaction.
+
+For the majority of reactions, the overheat temperature is 900K, and the pH range is 5-9, though it's always worth looking up the ranges as these are changing. Calomel is no different.
+
+To continue the tutorial, insert a beaker with at least 10u mercury and 10u chlorine added."}
+ if(TUT_HAS_REAGENTS) //10u Hg and Cl
+ data["tutorialMessage"] = {"Good job! You'll see that at present this isn't reacting. That's because this reaction needs a minimum temperature of 375K.
+
+For the most part the hotter your reaction is, the faster it will react when it’s past it’s minimum temperature. But be careful to not heat it too much! "If your reaction is slow, your temperature is too low"!
+
+When you’re ready, set your temperature to 375K and heat up the beaker to that amount."}
+ if(TUT_IS_ACTIVE) //heat 375K
+ data["tutorialMessage"] = {"Great! You should see your reaction slowly progressing.
+
+Notice the pH dial on the right; the sum pH should be slowly drifting towards the left on the dial. How pure your solution is at the end depends on how well you keep your reaction within the optimal pH range. The dial will flash if any of the present reactions are outside their optimal. "If you're getting sludge, give your pH a nudge"!
+
+In a moment, we’ll increase the temperature so that our rate is faster. It’s up to you to keep your pH within the limits, so keep an eye on that dial, and get ready to add basic buffer using the injection button to the left of the volume indicator.
+
+To continue set your target temperature to 390K."}
+ if(TUT_IS_REACTING) //Heat 390K
+ data["tutorialMessage"] = "Stay focused on the reaction! You can do it!"
+ if(TUT_FAIL) //Sludge
+ data["tutorialMessage"] = "Ah, unfortunately your purity was too low and the reaction fell apart into errant sludge. Don't worry, you can always try again! Be careful though, for some reactions, failing isn't nearly as forgiving."
+ if(TUT_COMPLETE) //Complete
+ var/datum/reagent/calo = beaker?.reagents.has_reagent(/datum/reagent/medicine/calomel)
+ if(!calo)
+ tutorial_state = TUT_COMPLETE
+ return
+ switch(calo.purity)
+ if(-INFINITY to 0.25)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. That's pretty close to the fail purity of 0.15 - which can often make some reactions explode. This chem will invert into Toxic sludge when ingested by another person, and will not cause of calomel's normal effects. Sneaky, huh?"
+ if(0.25 to 0.6)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Normally, this reaction will resolve above 0.7 without intervention. Are you praticing impure reactions? The lower you go, the higher change you have of getting dangerous effects during a reaction. In some more dangerous reactions, you're riding a fine line between death and an inverse chem, don't forget you can always chill your reaction to give yourself more time to manage it!"
+ if(0.6 to 0.75)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Normally, this reaction will resolve above 0.7 without intervention. Did you maybe add too much basic buffer and go past 9? If you like - you're welcome to try again. Just double press the help button!"
+ if(0.75 to 0.85)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. You got pretty close to optimal! Feel free to try again if you like by double pressing the help button."
+ if(0.75 to 0.99)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. You got pretty close to optimal! Feel free to try again if you like by double pressing the help button, but this is a respectable purity."
+ if(0.99 to 1)
+ data["tutorialMessage"] = "You did it! Congratulations! I can tell you that your final purity was [calo.purity]. Your calomel is as pure as they come! You've mastered the basics of chemistry, but there's plenty more challenges on the horizon. Good luck!"
+ user.client?.give_award(/datum/award/achievement/misc/chemistry_tut, user)
+ data["tutorialMessage"] += "\n\nDid you notice that your temperature increased past 390K while reacting too? That's because this reaction is exothermic (heat producing), so for some reactions you might have to adjust your target to compensate. Oh, and you can check your purity by researching and printing off a chemical analyzer at the medlathe (for now)!"
+ if(TUT_MISSING) //Missing
+ data["tutorialMessage"] = "Uh oh, something went wrong. Did you take the beaker out, heat it up too fast, or have other things in the beaker? Try restarting the tutorial by double pressing the help button."
- var/beakerContents[0]
- if(beaker)
- for(var/datum/reagent/R in beaker.reagents.reagent_list)
- beakerContents.Add(list(list("name" = R.name, "volume" = R.volume))) // list in a list because Byond merges the first list...
- data["beakerContents"] = beakerContents
return data
/obj/machinery/chem_heater/ui_act(action, params)
@@ -128,6 +381,103 @@
if(.)
target_temperature = clamp(target, 0, 1000)
if("eject")
- on = FALSE
+ //Eject doesn't turn it off, so you can preheat for beaker swapping
replace_beaker(usr)
. = TRUE
+ if("acidBuffer")
+ var/target = params["target"]
+ if(text2num(target) != null)
+ target = text2num(target)
+ . = TRUE
+ if(.)
+ move_buffer("acid", target)
+ if("basicBuffer")
+ var/target = params["target"]
+ if(text2num(target) != null)
+ target = text2num(target) //Because the input is flipped
+ . = TRUE
+ if(.)
+ move_buffer("basic", target)
+ if("disp_vol")
+ var/target = params["target"]
+ if(text2num(target) != null)
+ target = text2num(target) //Because the input is flipped
+ . = TRUE
+ if(.)
+ dispense_volume = target
+ if("help")
+ tutorial_active = !tutorial_active
+ if(tutorial_active)
+ tutorial_state = 1
+ return
+ tutorial_state = 0
+ //Refresh window size
+ ui_close(usr)
+ ui_interact(usr, null)
+
+
+///Moves a type of buffer from the heater to the beaker, or vice versa
+/obj/machinery/chem_heater/proc/move_buffer(buffer_type, volume)
+ if(!beaker)
+ say("No beaker found!")
+ return
+ if(buffer_type == "acid")
+ if(volume < 0)
+ var/datum/reagent/acid_reagent = beaker.reagents.get_reagent(/datum/reagent/reaction_agent/acidic_buffer)
+ if(!acid_reagent)
+ say("Unable to find acidic buffer in beaker to draw from! Please insert a beaker containing acidic buffer.")
+ return
+ var/datum/reagent/acid_reagent_heater = reagents.get_reagent(/datum/reagent/reaction_agent/acidic_buffer)
+ var/cur_vol = 0
+ if(acid_reagent_heater)
+ cur_vol = acid_reagent_heater.volume
+ volume = 100 - cur_vol
+ beaker.reagents.trans_id_to(src, acid_reagent.type, volume)//negative because we're going backwards
+ return
+ //We must be positive here
+ reagents.trans_id_to(beaker, /datum/reagent/reaction_agent/acidic_buffer, dispense_volume)
+ return
+
+ if(buffer_type == "basic")
+ if(volume < 0)
+ var/datum/reagent/basic_reagent = beaker.reagents.get_reagent(/datum/reagent/reaction_agent/basic_buffer)
+ if(!basic_reagent)
+ say("Unable to find basic buffer in beaker to draw from! Please insert a beaker containing basic buffer.")
+ return
+ var/datum/reagent/basic_reagent_heater = reagents.get_reagent(/datum/reagent/reaction_agent/basic_buffer)
+ var/cur_vol = 0
+ if(basic_reagent_heater)
+ cur_vol = basic_reagent_heater.volume
+ volume = 100 - cur_vol
+ beaker.reagents.trans_id_to(src, basic_reagent.type, volume)//negative because we're going backwards
+ return
+ reagents.trans_id_to(beaker, /datum/reagent/reaction_agent/basic_buffer, dispense_volume)
+ return
+
+
+/obj/machinery/chem_heater/proc/get_purity_color(datum/equilibrium/equilibrium)
+ var/_reagent = equilibrium.reaction.results[1]
+ var/datum/reagent/reagent = equilibrium.holder.get_reagent(_reagent)
+ switch(reagent.purity)
+ if(1 to INFINITY)
+ return "blue"
+ if(0.8 to 1)
+ return "green"
+ if(reagent.inverse_chem_val to 0.8)
+ return "olive"
+ if(equilibrium.reaction.purity_min to reagent.inverse_chem_val)
+ return "orange"
+ if(-INFINITY to equilibrium.reaction.purity_min)
+ return "red"
+
+//Has a lot of buffer and is upgraded
+/obj/machinery/chem_heater/debug
+ name = "Debug Reaction Chamber"
+ desc = "Now with even more buffers!"
+
+/obj/machinery/chem_heater/debug/Initialize()
+ . = ..()
+ reagents.maximum_volume = 2000
+ reagents.add_reagent(/datum/reagent/reaction_agent/basic_buffer, 980)
+ reagents.add_reagent(/datum/reagent/reaction_agent/acidic_buffer, 980)
+ heater_coefficient = 0.4 //hack way to upgrade
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index 627ef2a3a22292..425d111c1d51f2 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -195,7 +195,7 @@
/obj/machinery/chem_master/ui_data(mob/user)
var/list/data = list()
data["isBeakerLoaded"] = beaker ? 1 : 0
- data["beakerCurrentVolume"] = beaker ? beaker.reagents.total_volume : null
+ data["beakerCurrentVolume"] = beaker ? round(beaker.reagents.total_volume, 0.01) : null
data["beakerMaxVolume"] = beaker ? beaker.volume : null
data["mode"] = mode
data["condi"] = condi
@@ -213,13 +213,13 @@
var/beaker_contents[0]
if(beaker)
for(var/datum/reagent/R in beaker.reagents.reagent_list)
- beaker_contents.Add(list(list("name" = R.name, "id" = ckey(R.name), "volume" = R.volume))) // list in a list because Byond merges the first list...
+ beaker_contents.Add(list(list("name" = R.name, "id" = ckey(R.name), "volume" = round(R.volume, 0.01)))) // list in a list because Byond merges the first list...
data["beakerContents"] = beaker_contents
var/buffer_contents[0]
if(reagents.total_volume)
for(var/datum/reagent/N in reagents.reagent_list)
- buffer_contents.Add(list(list("name" = N.name, "id" = ckey(N.name), "volume" = N.volume))) // ^
+ buffer_contents.Add(list(list("name" = N.name, "id" = ckey(N.name), "volume" = round(N.volume, 0.01)))) // ^
data["bufferContents"] = buffer_contents
//Calculated at init time as it never changes
@@ -261,9 +261,15 @@
if (!beaker)
return FALSE
if (to_container == "buffer")
+ var/datum/reagent/R = beaker.reagents.get_reagent(reagent)
+ if(!check_reactions(R, beaker.reagents))
+ return FALSE
beaker.reagents.trans_id_to(src, reagent, amount)
return TRUE
if (to_container == "beaker" && mode)
+ var/datum/reagent/R = reagents.get_reagent(reagent)
+ if(!check_reactions(R, reagents))
+ return FALSE
reagents.trans_id_to(beaker, reagent, amount)
return TRUE
return FALSE
@@ -324,7 +330,7 @@
"Maximum [vol_each_max] units per item.",
"How many units to fill?",
vol_each_max))
- vol_each = clamp(vol_each, 0, vol_each_max)
+ vol_each = round(clamp(vol_each, 0, vol_each_max), 0.01)
if(vol_each <= 0)
return FALSE
// Get item name
@@ -420,7 +426,7 @@
state = "Gas"
var/const/P = 3 //The number of seconds between life ticks
var/T = initial(R.metabolization_rate) * (60 / P)
- analyze_vars = list("name" = initial(R.name), "state" = state, "color" = initial(R.color), "description" = initial(R.description), "metaRate" = T, "overD" = initial(R.overdose_threshold), "addicD" = initial(R.addiction_threshold))
+ analyze_vars = list("name" = initial(R.name), "state" = state, "color" = initial(R.color), "description" = initial(R.description), "metaRate" = T, "overD" = initial(R.overdose_threshold), "addicD" = initial(R.addiction_threshold), "pH" = initial(R.ph))
screen = "analyze"
return TRUE
@@ -569,6 +575,25 @@
container.lefthand_file = style["lefthand_file"]
container.righthand_file = style["righthand_file"]
+
+//Checks to see if the target reagent is being created (reacting) and if so prevents transfer
+//Only prevents reactant from being moved so that people can still manlipulate input reagents
+/obj/machinery/chem_master/proc/check_reactions(datum/reagent/reagent, datum/reagents/holder)
+ if(!reagent)
+ return FALSE
+ var/canMove = TRUE
+ for(var/e in holder.reaction_list)
+ var/datum/equilibrium/E = e
+ if(E.reaction.reaction_flags & REACTION_COMPETITIVE)
+ continue
+ for(var/result in E.reaction.required_reagents)
+ var/datum/reagent/R = result
+ if(R == reagent.type)
+ canMove = FALSE
+ if(!canMove)
+ say("Cannot move arrested chemical reaction reagents!")
+ return canMove
+
/**
* Machine that allows to identify and separate reagents in fitting container
* as well as to create new containers with separated reagents in it.
diff --git a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm
new file mode 100644
index 00000000000000..30bdf93ef11703
--- /dev/null
+++ b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm
@@ -0,0 +1,129 @@
+/*
+* A debug chem tester that will process through all recipies automatically and try to react them.
+* Highlights low purity reactions and and reactions that don't happen
+*/
+/obj/machinery/chem_recipe_debug
+ name = "chemical reaction tester"
+ density = TRUE
+ icon = 'icons/obj/chemical.dmi'
+ icon_state = "HPLC"
+ use_power = IDLE_POWER_USE
+ idle_power_usage = 40
+ resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE
+ ///List of every reaction in the game kept locally for easy access
+ var/list/cached_reactions = list()
+ ///What index in the cached_reactions we're in
+ var/index = 1
+ ///If the machine is currently processing through the list
+ var/processing = FALSE
+ ///Final output that highlights all of the reactions with inoptimal purity/voolume at base
+ var/problem_string
+ ///Final output that highlights all of the reactions with inoptimal purity/voolume at base
+ var/impure_string
+ ///The count of reactions that resolve between 1 - 0.9 purity
+ var/minorImpurity
+ ///The count of reactions that resolve below 0.9 purity
+ var/majorImpurity
+ ///If we failed to react this current chem so use a lower temp
+ var/failed = 0
+
+///Create reagents datum
+/obj/machinery/chem_recipe_debug/Initialize()
+ . = ..()
+ create_reagents(9000)//I want to make sure everything fits
+
+///Enable the machine
+/obj/machinery/chem_recipe_debug/attackby(obj/item/I, mob/user, params)
+ . = .()
+ if(processing)
+ say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]")
+ return
+ say("Starting processing")
+ setup_reactions()
+ begin_processing()
+
+///Enable the machine
+/obj/machinery/chem_recipe_debug/AltClick(mob/living/user)
+ . = ..()
+ if(processing)
+ say("currently processing reaction [index]: [cached_reactions[index]] of [cached_reactions.len]")
+ return
+ say("Starting processing")
+ setup_reactions()
+ begin_processing()
+
+///Resets the index, and creates the cached_reaction list from all possible reactions
+/obj/machinery/chem_recipe_debug/proc/setup_reactions()
+ cached_reactions = list()
+ for(var/V in GLOB.chemical_reactions_list)
+ if(is_type_in_list(GLOB.chemical_reactions_list[V], cached_reactions))
+ continue
+ cached_reactions += GLOB.chemical_reactions_list[V]
+ reagents.clear_reagents()
+ index = 1
+ processing = TRUE
+
+/*
+* The main loop that sets up, creates and displays results from a reaction
+* warning: this code is a hot mess
+*/
+/obj/machinery/chem_recipe_debug/process(delta_time)
+ if(processing == FALSE)
+ setup_reactions()
+ if(reagents.is_reacting == TRUE)
+ return
+ if(index >= cached_reactions.len)
+ say("Completed testing, missing reactions products (may have exploded) are:")
+ say("[problem_string]")
+ say("Problem with results are:")
+ say("[impure_string]")
+ say("Reactions with minor impurity: [minorImpurity], reactions with major impurity: [majorImpurity]")
+ processing = FALSE
+ end_processing()
+ if(reagents.reagent_list)
+ say("Reaction completed for [cached_reactions[index]] final temperature = [reagents.chem_temp], ph = [reagents.ph].")
+ var/datum/chemical_reaction/C = cached_reactions[index]
+ for(var/R in C.results)
+ var/datum/reagent/R2 = reagents.get_reagent(R)
+ if(!R2)
+ say("Unable to find product [R] in holder after reaction! reagents found are:")
+ for(var/R3 in reagents.reagent_list)
+ say("[R3]")
+ var/obj/item/reagent_containers/glass/beaker/bluespace/B = new /obj/item/reagent_containers/glass/beaker/bluespace(loc)
+ reagents.trans_to(B)
+ B.name = "[cached_reactions[index]]"
+ if(failed > 0)
+ problem_string += "[cached_reactions[index]] Unable to find product [R] in holder after reaction! index:[index]\n"
+ failed++
+ continue
+ say("Reaction has a product [R] [R2.volume]u purity of [R2.purity]")
+ if(R2.purity < 0.9)
+ impure_string += "Reaction [cached_reactions[index]] has a product [R] [R2.volume]u purity of [R2.purity] index:[index]\n"
+ majorImpurity++
+ else if (R2.purity < 1)
+ impure_string += "Reaction [cached_reactions[index]] has a product [R] [R2.volume]u purity of [R2.purity] index:[index]\n"
+ minorImpurity++
+ if(R2.volume < C.results[R])
+ impure_string += "Reaction [cached_reactions[index]] has a product [R] [R2.volume]u purity of [R2.purity] index:[index]\n"
+ reagents.clear_reagents()
+ if(failed > 1)
+ index++
+ failed = 0
+ if(failed == 0)
+ index++
+ var/datum/chemical_reaction/C = cached_reactions[index]
+ if(!C)
+ say("Unable to find reaction on index: [index]")
+ for(var/R in C.required_reagents)
+ reagents.add_reagent(R, C.required_reagents[R]*20)
+ for(var/cat in C.required_catalysts)
+ reagents.add_reagent(cat, C.required_catalysts[cat])
+ if(failed == 0)
+ reagents.chem_temp = C.optimal_temp
+ if(failed == 1)
+ reagents.chem_temp = C.required_temp+25
+ failed++
+ say("Reacting [cached_reactions[index]] starting pH: [reagents.ph] index [index] of [cached_reactions.len]")
+ if(C.reaction_flags & REACTION_INSTANT)
+ say("This reaction is instant")
+
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index 480f5f949f2aff..59d04f3d8f02b3 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -44,6 +44,12 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
var/current_cycle = 0
///pretend this is moles
var/volume = 0
+ /// pH of the reagent
+ var/ph = 7
+ ///Purity of the reagent - for use with internal reaction mechanics only. Use below (creation_purity) if you're writing purity effects into a reagent's use mechanics.
+ 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
/// color it looks in containers etc
var/color = "#000000" // rgb: 0, 0, 0
/// can this reagent be synthesized? (for example: odysseus syringe gun)
@@ -76,6 +82,17 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
var/list/reagent_removal_skip_list = list()
///The set of exposure methods this penetrates skin with.
var/penetrates_skin = VAPOR
+ /// See fermi_readme.dm REAGENT_DEAD_PROCESS, REAGENT_DONOTSPLIT, REAGENT_INVISIBLE, REAGENT_SNEAKYNAME, REAGENT_SPLITRETAINVOL
+ var/chemical_flags = NONE
+ ///impure chem values (see fermi_readme.dm for more details on impure/inverse/failed mechanics):
+ /// What chemical path is made when metabolised as a function of purity
+ var/impure_chem = /datum/reagent/impurity
+ /// If the impurity is below 0.5, replace ALL of the chem with inverse_chem upon metabolising
+ var/inverse_chem_val = 0.25
+ /// What chem is metabolised when purity is below inverse_chem_val
+ var/inverse_chem = /datum/reagent/impurity/toxic
+ ///what chem is made at the end of a reaction IF the purity is below the recipies purity_min at the END of a reaction only
+ var/failed_chem = /datum/reagent/consumable/failed_reaction
/datum/reagent/New()
SHOULD_CALL_PARENT(TRUE)
@@ -128,13 +145,19 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
return
holder.remove_reagent(type, metabolization_rate * M.metabolism_efficiency) //By default it slowly disappears.
+/*
+Used to run functions before a reagent is transfered. Returning TRUE will block the transfer attempt.
+Primarily used in reagents/reaction_agents
+*/
+/datum/reagent/proc/intercept_reagents_transfer(datum/reagents/target)
+ return FALSE
+
///Called after a reagent is transfered
/datum/reagent/proc/on_transfer(atom/A, methods=TOUCH, trans_volume)
return
-
/// Called when this reagent is first added to a mob
-/datum/reagent/proc/on_mob_add(mob/living/L)
+/datum/reagent/proc/on_mob_add(mob/living/L, amount)
return
/// Called when this reagent is removed while inside a mob
@@ -149,6 +172,15 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
/datum/reagent/proc/on_mob_end_metabolize(mob/living/L)
return
+/// Called when a reagent is inside of a mob when they are dead
+/datum/reagent/proc/on_mob_dead(mob/living/carbon/C)
+ if(!(chemical_flags & REAGENT_DEAD_PROCESS))
+ return
+ current_cycle++
+ if(length(reagent_removal_skip_list))
+ return
+ holder.remove_reagent(type, metabolization_rate * C.metabolism_efficiency)
+
/// Called by [/datum/reagents/proc/conditional_update_move]
/datum/reagent/proc/on_move(mob/M)
return
@@ -158,7 +190,7 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
return
/// Called when two reagents of the same are mixing.
-/datum/reagent/proc/on_merge(data)
+/datum/reagent/proc/on_merge(data, amount)
return
/// Called by [/datum/reagents/proc/conditional_update]
diff --git a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
index 588101a9b19396..1bafe4fe107b75 100644
--- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
@@ -13,6 +13,7 @@
nutriment_factor = 0
taste_description = "alcohol"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
+ ph = 7.33
var/boozepwr = 65 //Higher numbers equal higher hardness, higher hardness equals more intense alcohol poisoning
/*
@@ -88,6 +89,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
taste_description = "piss water"
glass_name = "glass of beer"
glass_desc = "A freezing pint of beer."
+ ph = 4
// Beer is a chemical composition of alcohol and various other things. It's a garbage nutrient but hey, it's still one. Also alcohol is bad, mmmkay?
@@ -104,6 +106,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
taste_description = "dish water"
glass_name = "glass of light beer"
glass_desc = "A freezing pint of watery light beer."
+ ph = 5
/datum/reagent/consumable/ethanol/beer/maltliquor
name = "Malt Liquor"
@@ -112,6 +115,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
taste_description = "sweet corn beer and the hood life"
glass_name = "glass of malt liquor"
glass_desc = "A freezing pint of malt liquor."
+ ph = 4.8
/datum/reagent/consumable/ethanol/beer/green
name = "Green Beer"
@@ -121,6 +125,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "greenbeerglass"
glass_name = "glass of green beer"
glass_desc = "A freezing pint of green beer. Festive."
+ ph = 6
/datum/reagent/consumable/ethanol/beer/green/on_mob_life(mob/living/carbon/M)
@@ -140,6 +145,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of RR coffee liquor"
glass_desc = "DAMN, THIS THING LOOKS ROBUST!"
shot_glass_icon_state = "shotglasscream"
+ ph = 6
/datum/reagent/consumable/ethanol/kahlua/on_mob_life(mob/living/carbon/M)
M.dizziness = max(0,M.dizziness-5)
@@ -160,6 +166,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of whiskey"
glass_desc = "The silky, smokey whiskey goodness inside the glass makes the drink look very classy."
shot_glass_icon_state = "shotglassbrown"
+ ph = 4.5
/datum/reagent/consumable/ethanol/whiskey/kong
name = "Kong"
@@ -284,6 +291,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of vodka"
glass_desc = "The glass contain wodka. Xynta."
shot_glass_icon_state = "shotglassclear"
+ ph = 8.1
/datum/reagent/consumable/ethanol/vodka/on_mob_life(mob/living/carbon/M)
M.radiation = max(M.radiation-2,0)
@@ -316,6 +324,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "threemileislandglass"
glass_name = "Three Mile Island Ice Tea"
glass_desc = "A glass of this is sure to prevent a meltdown."
+ ph = 3.5
/datum/reagent/consumable/ethanol/threemileisland/on_mob_life(mob/living/carbon/M)
M.set_drugginess(50)
@@ -330,6 +339,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "ginvodkaglass"
glass_name = "glass of gin"
glass_desc = "A crystal clear glass of Griffeater gin."
+ ph = 6.9
/datum/reagent/consumable/ethanol/rum
name = "Rum"
@@ -341,6 +351,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of rum"
glass_desc = "Now you want to Pray for a pirate suit, don't you?"
shot_glass_icon_state = "shotglassbrown"
+ ph = 6.5
/datum/reagent/consumable/ethanol/tequila
name = "Tequila"
@@ -352,6 +363,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of tequila"
glass_desc = "Now all that's missing is the weird colored shades!"
shot_glass_icon_state = "shotglassgold"
+ ph = 4
/datum/reagent/consumable/ethanol/vermouth
name = "Vermouth"
@@ -363,6 +375,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of vermouth"
glass_desc = "You wonder why you're even drinking this straight."
shot_glass_icon_state = "shotglassclear"
+ ph = 3.25
/datum/reagent/consumable/ethanol/wine
name = "Wine"
@@ -374,6 +387,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of wine"
glass_desc = "A very classy looking drink."
shot_glass_icon_state = "shotglassred"
+ ph = 3.45
/datum/reagent/consumable/ethanol/wine/on_merge(data)
. = ..()
@@ -395,6 +409,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
boozepwr = 45
quality = DRINK_FANTASTIC
taste_description = "scaley sweetness"
+ ph = 3
/datum/reagent/consumable/ethanol/grappa
name = "Grappa"
@@ -405,6 +420,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "grappa"
glass_name = "glass of grappa"
glass_desc = "A fine drink originally made to prevent waste by using the leftovers from winemaking."
+ ph = 3.5
/datum/reagent/consumable/ethanol/amaretto
name = "Amaretto"
@@ -427,6 +443,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of cognac"
glass_desc = "Damn, you feel like some kind of French aristocrat just by holding this."
shot_glass_icon_state = "shotglassbrown"
+ ph = 3.5
/datum/reagent/consumable/ethanol/absinthe
name = "Absinthe"
@@ -470,6 +487,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "aleglass"
glass_name = "glass of ale"
glass_desc = "A freezing pint of delicious Ale."
+ ph = 4.5
/datum/reagent/consumable/ethanol/goldschlager
name = "Goldschlager"
@@ -531,6 +549,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "glass of patron"
glass_desc = "Drinking patron in the bar, with all the subpar ladies."
shot_glass_icon_state = "shotglassclear"
+ ph = 4.5
/datum/reagent/consumable/ethanol/gintonic
name = "Gin and Tonic"
@@ -542,6 +561,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "gintonicglass"
glass_name = "Gin and Tonic"
glass_desc = "A mild but still great cocktail. Drink up, like a true Englishman."
+ ph = 3
/datum/reagent/consumable/ethanol/rum_coke
name = "Rum and Coke"
@@ -553,6 +573,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_icon_state = "whiskeycolaglass"
glass_name = "Rum and Coke"
glass_desc = "The classic go-to of space-fratboys."
+ ph = 4
/datum/reagent/consumable/ethanol/cuba_libre
name = "Cuba Libre"
@@ -743,6 +764,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "Heavy, hot and strong. Just like the Iron fist of the LAW."
overdose_threshold = 40
var/datum/brain_trauma/special/beepsky/B
+ ph = 2
/datum/reagent/consumable/ethanol/beepsky_smash/on_mob_metabolize(mob/living/carbon/M)
if(HAS_TRAIT(M, TRAIT_ALCOHOL_TOLERANCE))
@@ -1656,6 +1678,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_name = "Bastion Bourbon"
glass_desc = "If you're feeling low, count on the buttery flavor of our own bastion bourbon."
shot_glass_icon_state = "shotglassgreen"
+ ph = 4
/datum/reagent/consumable/ethanol/bastion_bourbon/on_mob_metabolize(mob/living/L)
var/heal_points = 10
@@ -1986,6 +2009,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
can_synth = FALSE
var/list/names = list("null fruit" = 1) //Names of the fruits used. Associative list where name is key, value is the percentage of that fruit.
var/list/tastes = list("bad coding" = 1) //List of tastes. See above.
+ ph = 4
/datum/reagent/consumable/ethanol/fruit_wine/on_new(list/data)
names = data["names"]
@@ -1995,6 +2019,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
generate_data_info(data)
/datum/reagent/consumable/ethanol/fruit_wine/on_merge(list/data, amount)
+ ..()
var/diff = (amount/volume)
if(diff < 1)
color = BlendRGB(color, data["color"], diff/2) //The percentage difference over two, so that they take average if equal.
@@ -2078,7 +2103,6 @@ All effects don't start immediately, but rather get worse over time; the rate is
flavor += english_list(secondary_tastes)
taste_description = flavor
-
/datum/reagent/consumable/ethanol/champagne //How the hell did we not have champagne already!?
name = "Champagne"
description = "A sparkling wine known for its ability to strike fast and hard."
diff --git a/code/modules/reagents/chemistry/reagents/catalyst_reagents.dm b/code/modules/reagents/chemistry/reagents/catalyst_reagents.dm
new file mode 100644
index 00000000000000..6e4e2b56c7ed00
--- /dev/null
+++ b/code/modules/reagents/chemistry/reagents/catalyst_reagents.dm
@@ -0,0 +1,48 @@
+///These alter reaction conditions while they're in the beaker
+/datum/reagent/catalyst_agent
+ name ="Catalyst agent"
+ ///The typepath of the reagent they that they affect
+ var/target_reagent_type
+ ///The minimumvolume required in the beaker for them to have an effect
+ var/min_volume = 10
+ ///The value in which the associated type is modified
+ var/modifier = 1
+
+/datum/reagent/catalyst_agent/proc/consider_catalyst(datum/equilibrium/equilibrium)
+ for(var/_product in equilibrium.reaction.results)
+ if(ispath(_product, target_reagent_type))
+ return TRUE
+ return FALSE
+
+/datum/reagent/catalyst_agent/speed
+ name ="Speed catalyst agent"
+
+/datum/reagent/catalyst_agent/speed/consider_catalyst(datum/equilibrium/equilibrium)
+ . = ..()
+ if(.)
+ equilibrium.speed_mod = creation_purity*modifier //So a purity 1 = the modifier, and a purity 0 = the inverse modifier. For this we don't want a negative speed_mod (I have no idea what happens if we do)
+ equilibrium.time_deficit += (creation_purity)*(0.05 * modifier) //give the reaction a little boost too (40% faster)
+
+/datum/reagent/catalyst_agent/ph
+ name ="pH catalyst agent"
+
+/datum/reagent/catalyst_agent/ph/consider_catalyst(datum/equilibrium/equilibrium)
+ . = ..()
+ if(.)
+ equilibrium.h_ion_mod = ((creation_purity-0.5)*2)*modifier //So a purity 1 = the modifier, and a purity 0 = the inverse modifier
+
+/datum/reagent/catalyst_agent/temperature
+ name = "Temperature catalyst agent"
+
+/datum/reagent/catalyst_agent/temperature/consider_catalyst(datum/equilibrium/equilibrium)
+ . = ..()
+ if(.)
+ equilibrium.thermic_mod = ((creation_purity-0.5)*2)*modifier //So a purity 1 = the modifier, and a purity 0 = the inverse modifier
+
+///These affect medicines
+/datum/reagent/catalyst_agent/speed/medicine
+ name = "Palladium synthate catalyst"
+ target_reagent_type = /datum/reagent/medicine
+ modifier = 2
+ ph = 2 //drift towards acidity
+ color = "#b1b1b1"
diff --git a/code/modules/reagents/chemistry/reagents/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drink_reagents.dm
index 6e0bb7c6e372a8..cae3737431ec83 100644
--- a/code/modules/reagents/chemistry/reagents/drink_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drink_reagents.dm
@@ -12,6 +12,7 @@
glass_icon_state = "glass_orange"
glass_name = "glass of orange juice"
glass_desc = "Vitamins! Yay!"
+ ph = 3.3
/datum/reagent/consumable/orangejuice/on_mob_life(mob/living/carbon/M)
if(M.getOxyLoss() && prob(30))
@@ -42,6 +43,7 @@
glass_icon_state = "glass_green"
glass_name = "glass of lime juice"
glass_desc = "A glass of sweet-sour lime juice."
+ ph = 2.2
/datum/reagent/consumable/limejuice/on_mob_life(mob/living/carbon/M)
if(M.getToxLoss() && prob(20))
@@ -84,6 +86,7 @@
description = "The sweet juice of an apple, fit for all ages."
color = "#ECFF56" // rgb: 236, 255, 86
taste_description = "apples"
+ ph = 3.2 // ~ 2.7 -> 3.7
/datum/reagent/consumable/poisonberryjuice
name = "Poison Berry Juice"
@@ -116,6 +119,7 @@
glass_icon_state = "lemonglass"
glass_name = "glass of lemon juice"
glass_desc = "Sour..."
+ ph = 2
/datum/reagent/consumable/banana
name = "Banana Juice"
@@ -199,6 +203,7 @@
glass_icon_state = "glass_white"
glass_name = "glass of milk"
glass_desc = "White and nutritious goodness!"
+ ph = 6.5
// Milk is good for humans, but bad for plants. The sugars cannot be used by plants, and the milk fat harms growth. Not shrooms though. I can't deal with this now...
/datum/reagent/consumable/milk/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index 0137d78c98189a..3ebae322563655 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -13,6 +13,7 @@
description = "An illegal chemical compound used as drug."
color = "#60A584" // rgb: 96, 165, 132
overdose_threshold = 30
+ ph = 9
/datum/reagent/drug/space_drugs/on_mob_life(mob/living/carbon/M)
M.set_drugginess(15)
@@ -43,6 +44,7 @@
trippy = FALSE
overdose_threshold=15
metabolization_rate = 0.125 * REAGENTS_METABOLISM
+ ph = 8
//Nicotine is used as a pesticide IRL.
/datum/reagent/drug/nicotine/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -77,6 +79,7 @@
color = "#FA00C8"
overdose_threshold = 20
addiction_threshold = 10
+ ph = 10
/datum/reagent/drug/crank/on_mob_life(mob/living/carbon/M)
if(prob(5))
@@ -126,6 +129,7 @@
color = "#0064B4"
overdose_threshold = 20
addiction_threshold = 15
+ ph = 9
/datum/reagent/drug/krokodil/on_mob_life(mob/living/carbon/M)
@@ -178,6 +182,7 @@
overdose_threshold = 20
addiction_threshold = 10
metabolization_rate = 0.75 * REAGENTS_METABOLISM
+ ph = 5
/datum/reagent/drug/methamphetamine/on_mob_metabolize(mob/living/L)
..()
@@ -263,6 +268,7 @@
addiction_threshold = 10
taste_description = "salt" // because they're bathsalts?
var/datum/brain_trauma/special/psychotic_brawling/bath_salts/rage
+ ph = 8.2
/datum/reagent/drug/bath_salts/on_mob_metabolize(mob/living/L)
..()
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index d271c35184525b..061a53496845e8 100755
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -13,6 +13,10 @@
taste_mult = 4
var/nutriment_factor = 1 * REAGENTS_METABOLISM
var/quality = 0 //affects mood, typically higher for mixed drinks with more complex recipes
+ impure_chem = /datum/reagent/water
+ inverse_chem_val = 0.1
+ inverse_chem = /datum/reagent/water
+ failed_chem = /datum/reagent/consumable/nutriment
/datum/reagent/consumable/on_mob_life(mob/living/carbon/M)
current_cycle++
@@ -74,6 +78,7 @@
data = counterlist_normalise(supplied_data)
/datum/reagent/consumable/nutriment/on_merge(list/newdata, newvolume)
+ . = ..()
if(!islist(newdata) || !newdata.len)
return
@@ -268,6 +273,7 @@
description = "A special oil that noticeably chills the body. Extracted from chilly peppers and slimes."
color = "#8BA6E9" // rgb: 139, 166, 233
taste_description = "mint"
+ ph = 13 //HMM! I wonder
/datum/reagent/consumable/frostoil/on_mob_life(mob/living/carbon/M)
var/cooling = 0
@@ -316,6 +322,7 @@
color = "#B31008" // rgb: 179, 16, 8
taste_description = "scorching agony"
penetrates_skin = NONE
+ ph = 7.4
/datum/reagent/consumable/condensedcapsaicin/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -391,6 +398,7 @@
color = "#E700E7" // rgb: 231, 0, 231
metabolization_rate = 0.2 * REAGENTS_METABOLISM
taste_description = "mushroom"
+ ph = 11
/datum/reagent/drug/mushroomhallucinogen/on_mob_life(mob/living/carbon/M)
if(!M.slurring)
@@ -634,6 +642,7 @@
description = "A blinding substance extracted from certain onions."
color = "#c0c9a0"
taste_description = "bitterness"
+ ph = 5
/datum/reagent/consumable/tearjuice/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -677,6 +686,7 @@
description = "An ichor, derived from a certain mushroom, makes for a bad time."
color = "#1d043d"
taste_description = "bitter mushroom"
+ ph = 12
/datum/reagent/consumable/entpoly/on_mob_life(mob/living/carbon/M)
if(current_cycle >= 10)
@@ -697,6 +707,7 @@
description = "A stimulating ichor which causes luminescent fungi to grow on the skin. "
color = "#b5a213"
taste_description = "tingling mushroom"
+ ph = 11.2
//Lazy list of mobs affected by the luminosity of this reagent.
var/list/mobs_affected
@@ -730,6 +741,7 @@
color = "#d3a308"
nutriment_factor = 3 * REAGENTS_METABOLISM
taste_description = "fruity mushroom"
+ ph = 10.4
/datum/reagent/consumable/vitfro/on_mob_life(mob/living/carbon/M)
if(prob(80))
@@ -744,6 +756,7 @@
nutriment_factor = 5 * REAGENTS_METABOLISM
color = "#eef442" // rgb: 238, 244, 66
taste_description = "mournful honking"
+ ph = 9.2
/datum/reagent/consumable/liquidelectricity
@@ -795,6 +808,7 @@
quality = FOOD_AMAZING
taste_mult = 100
can_synth = FALSE
+ ph = 6.1
/datum/reagent/consumable/nutriment/peptides
name = "Peptides"
diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents.dm
new file mode 100644
index 00000000000000..92cea81f3df2f2
--- /dev/null
+++ b/code/modules/reagents/chemistry/reagents/impure_reagents.dm
@@ -0,0 +1,39 @@
+//Reagents produced by metabolising/reacting fermichems inoptimally, i.e. inverse_chems or impure_chems
+//Inverse = Splitting
+//Invert = Whole conversion
+
+//Causes slight liver damage, and that's it.
+/datum/reagent/impurity
+ name = "Chemical Isomers"
+ description = "Impure chemical isomers made from inoptimal reactions. Causes mild liver damage"
+ //by default, it will stay hidden on splitting, but take the name of the source on inverting. Cannot be fractioned down either if the reagent is somehow isolated.
+ chemical_flags = REAGENT_INVISIBLE | REAGENT_SNEAKYNAME | REAGENT_DONOTSPLIT
+ ph = 3
+ overdose_threshold = 0 //So that they're shown as a problem (?)
+
+/datum/reagent/impurity/on_mob_life(mob/living/carbon/C)
+ var/obj/item/organ/liver/L = C.getorganslot(ORGAN_SLOT_LIVER)
+ if(!L)//Though, lets be safe
+ C.adjustToxLoss(1, FALSE)//Incase of no liver!
+ return ..()
+ C.adjustOrganLoss(ORGAN_SLOT_LIVER, 0.5*REM)
+ return ..()
+
+/datum/reagent/impurity/toxic
+ name = "Toxic sludge"
+ description = "Toxic chemical isomers made from impure reactions. Causes toxin damage"
+ ph = 2
+
+/datum/reagent/impurity/toxic/on_mob_life(mob/living/carbon/C)
+ C.adjustToxLoss(1, FALSE)
+ return ..()
+
+//technically not a impure chem, but it's here because it can only be made with a failed impure reaction
+/datum/reagent/consumable/failed_reaction
+ name = "Viscous sludge"
+ description = "A off smelling sludge that's created when a reaction gets too impure."
+ nutriment_factor = -1
+ quality = -1
+ ph = 1.5
+ taste_description = "an awful, strongly chemical taste"
+ color = "#270d03"
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index f9c760893ace15..874e8f5dc9698d 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -19,6 +19,7 @@
/datum/reagent/medicine/leporazine
name = "Leporazine"
description = "Leporazine will effectively regulate a patient's body temperature, ensuring it never leaves safe levels."
+ ph = 8.4
color = "#DB90C6"
/datum/reagent/medicine/leporazine/on_mob_life(mob/living/carbon/M)
@@ -109,6 +110,7 @@
name = "Synaptizine"
description = "Increases resistance to stuns as well as reducing drowsiness and hallucinations."
color = "#FF00FF"
+ ph = 4
/datum/reagent/medicine/synaptizine/on_mob_life(mob/living/carbon/M)
M.drowsyness = max(M.drowsyness-5, 0)
@@ -129,6 +131,7 @@
name = "Diphen-Synaptizine"
description = "Reduces drowsiness, hallucinations, and Histamine from body."
color = "#EC536D" // rgb: 236, 83, 109
+ ph = 5.2
/datum/reagent/medicine/synaphydramine/on_mob_life(mob/living/carbon/M)
M.drowsyness = max(M.drowsyness-5, 0)
@@ -147,6 +150,7 @@
description = "A chemical mixture with almost magical healing powers. Its main limitation is that the patient's body temperature must be under 270K for it to metabolise correctly."
color = "#0000C8"
taste_description = "blue"
+ ph = 11
/datum/reagent/medicine/cryoxadone/on_mob_life(mob/living/carbon/M)
var/power = -0.00003 * (M.bodytemperature ** 2) + 3
@@ -175,6 +179,7 @@
description = "A chemical that derives from Cryoxadone. It specializes in healing clone damage, but nothing else. Requires very cold temperatures to properly metabolize, and metabolizes quicker than cryoxadone."
color = "#3D3DC6"
taste_description = "muscle"
+ ph = 13
metabolization_rate = 1.5 * REAGENTS_METABOLISM
/datum/reagent/medicine/clonexadone/on_mob_life(mob/living/carbon/M)
@@ -190,6 +195,7 @@
description = "A mixture of cryoxadone and slime jelly, that apparently inverses the requirement for its activation."
color = "#f7832a"
taste_description = "spicy jelly"
+ ph = 12
/datum/reagent/medicine/pyroxadone/on_mob_life(mob/living/carbon/M)
if(M.bodytemperature > BODYTEMP_HEAT_DAMAGE_LIMIT)
@@ -222,6 +228,7 @@
reagent_state = SOLID
color = "#669900" // rgb: 102, 153, 0
overdose_threshold = 30
+ ph = 12.2
taste_description = "fish"
/datum/reagent/medicine/rezadone/on_mob_life(mob/living/carbon/M)
@@ -253,6 +260,7 @@
description = "Spaceacillin will prevent a patient from conventionally spreading any diseases they are currently infected with. Also reduces infection in serious burns."
color = "#E1F2E6"
metabolization_rate = 0.1 * REAGENTS_METABOLISM
+ ph = 8.1
//Goon Chems. Ported mainly from Goonstation. Easily mixable (or not so easily) and provide a variety of effects.
@@ -263,6 +271,7 @@
color = "#1E8BFF"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
overdose_threshold = 25
+ ph = 10.7
/datum/reagent/medicine/oxandrolone/on_mob_life(mob/living/carbon/M)
if(M.getFireLoss() > 25)
@@ -289,6 +298,7 @@
var/last_added = 0
var/maximum_reachable = BLOOD_VOLUME_NORMAL - 10 //So that normal blood regeneration can continue with salglu active
var/extra_regen = 0.25 // in addition to acting as temporary blood, also add this much to their actual blood per tick
+ ph = 5.5
/datum/reagent/medicine/salglu_solution/on_mob_life(mob/living/carbon/M)
if(last_added)
@@ -326,6 +336,7 @@
reagent_state = LIQUID
color = "#6D6374"
metabolization_rate = 0.4 * REAGENTS_METABOLISM
+ ph = 2.6
/datum/reagent/medicine/mine_salve/on_mob_life(mob/living/carbon/C)
C.hal_screwyhud = SCREWYHUD_HEALTHY
@@ -367,6 +378,7 @@
metabolization_rate = 0.25 * REAGENTS_METABOLISM
overdose_threshold = 30
var/healing = 0.5
+ ph = 2
/datum/reagent/medicine/omnizine/on_mob_life(mob/living/carbon/M)
M.adjustToxLoss(-healing*REM, 0)
@@ -397,6 +409,7 @@
color = "#19C832"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
taste_description = "acid"
+ ph = 1.5
/datum/reagent/medicine/calomel/on_mob_life(mob/living/carbon/M)
for(var/datum/reagent/toxin/R in M.reagents.reagent_list)
@@ -412,6 +425,7 @@
reagent_state = LIQUID
color = "#BAA15D"
metabolization_rate = 2 * REAGENTS_METABOLISM
+ ph = 12 //It's a reducing agent
/datum/reagent/medicine/potass_iodide/on_mob_life(mob/living/carbon/M)
if(M.radiation > 0)
@@ -424,6 +438,7 @@
reagent_state = LIQUID
color = "#E6FFF0"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
+ ph = 1 //One of the best buffers, NEVERMIND!
/datum/reagent/medicine/pen_acid/on_mob_life(mob/living/carbon/M)
M.radiation -= max(M.radiation-RAD_MOB_SAFE, 0)/50
@@ -441,6 +456,7 @@
color = "#D2D2D2"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
overdose_threshold = 25
+ ph = 2.1
/datum/reagent/medicine/sal_acid/on_mob_life(mob/living/carbon/M)
if(M.getBruteLoss() > 25)
@@ -462,6 +478,7 @@
reagent_state = LIQUID
color = "#00FFFF"
metabolization_rate = 0.25 * REAGENTS_METABOLISM
+ ph = 2
/datum/reagent/medicine/salbutamol/on_mob_life(mob/living/carbon/M)
M.adjustOxyLoss(-3*REM, 0)
@@ -478,6 +495,7 @@
metabolization_rate = 0.5 * REAGENTS_METABOLISM
overdose_threshold = 30
addiction_threshold = 25
+ ph = 12
/datum/reagent/medicine/ephedrine/on_mob_metabolize(mob/living/L)
..()
@@ -571,6 +589,7 @@
reagent_state = LIQUID
color = "#64FFE6"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
+ ph = 11.5
/datum/reagent/medicine/diphenhydramine/on_mob_life(mob/living/carbon/M)
if(prob(10))
@@ -587,6 +606,7 @@
metabolization_rate = 0.5 * REAGENTS_METABOLISM
overdose_threshold = 30
addiction_threshold = 25
+ ph = 8.96
/datum/reagent/medicine/morphine/on_mob_metabolize(mob/living/L)
..()
@@ -656,6 +676,7 @@
color = "#404040" //oculine is dark grey, inacusiate is light grey
metabolization_rate = 0.25 * REAGENTS_METABOLISM
taste_description = "dull toxin"
+ ph = 10
/datum/reagent/medicine/oculine/on_mob_life(mob/living/carbon/M)
var/obj/item/organ/eyes/eyes = M.getorganslot(ORGAN_SLOT_EYES)
@@ -680,6 +701,7 @@
name = "Inacusiate"
description = "Rapidly repairs damage to the patient's ears to cure deafness, assuming the source of said deafness isn't from genetic mutations, chronic deafness, or a total defecit of ears." //by "chronic" deafness, we mean people with the "deaf" quirk
color = "#606060" // ditto
+ ph = 2
/datum/reagent/medicine/inacusiate/on_mob_life(mob/living/carbon/M)
var/obj/item/organ/ears/ears = M.getorganslot(ORGAN_SLOT_EARS)
@@ -693,6 +715,7 @@
color = "#1D3535" //slightly more blue, like epinephrine
metabolization_rate = 0.25 * REAGENTS_METABOLISM
overdose_threshold = 35
+ ph = 12
/datum/reagent/medicine/atropine/on_mob_life(mob/living/carbon/M)
if(M.health <= M.crit_threshold)
@@ -721,6 +744,7 @@
color = "#D2FFFA"
metabolization_rate = 0.25 * REAGENTS_METABOLISM
overdose_threshold = 30
+ ph = 10.2
/datum/reagent/medicine/epinephrine/on_mob_metabolize(mob/living/carbon/M)
..()
@@ -769,6 +793,7 @@
metabolization_rate = 1.25 * REAGENTS_METABOLISM
taste_description = "magnets"
harmful = TRUE
+ ph = 0.5
// FEED ME SEYMOUR
@@ -810,6 +835,7 @@
name = "Mannitol"
description = "Efficiently restores brain damage."
color = "#A0A0A0" //mannitol is light grey, neurine is lighter grey
+ ph = 10.4
/datum/reagent/medicine/mannitol/on_mob_life(mob/living/carbon/C)
C.adjustOrganLoss(ORGAN_SLOT_BRAIN, -2*REM)
@@ -841,6 +867,7 @@
description = "Removes jitteriness and restores genetic defects."
color = "#5096C8"
taste_description = "acid"
+ ph = 2
/datum/reagent/medicine/mutadone/on_mob_life(mob/living/carbon/M)
M.jitteriness = 0
@@ -854,6 +881,7 @@
description = "Purges alcoholic substance from the patient's body and eliminates its side effects."
color = "#00B4C8"
taste_description = "raw egg"
+ ph = 4
/datum/reagent/medicine/antihol/on_mob_life(mob/living/carbon/M)
M.dizziness = 0
@@ -874,6 +902,7 @@
color = "#78008C"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
overdose_threshold = 60
+ ph = 8.7
/datum/reagent/medicine/stimulants/on_mob_metabolize(mob/living/L)
..()
@@ -910,6 +939,7 @@
reagent_state = LIQUID
color = "#FFFFF0"
metabolization_rate = 0.5 * REAGENTS_METABOLISM
+ ph = 6.7
/datum/reagent/medicine/insulin/on_mob_life(mob/living/carbon/M)
if(M.AdjustSleeping(-20))
@@ -924,6 +954,7 @@
description = "Stabilizes the breathing of patients. Good for those in critical condition."
reagent_state = LIQUID
color = "#A4D8D8"
+ ph = 8.5
/datum/reagent/medicine/inaprovaline/on_mob_life(mob/living/carbon/M)
if(M.losebreath >= 5)
@@ -961,6 +992,7 @@
reagent_state = SOLID
color = "#555555"
overdose_threshold = 30
+ ph = 11
/datum/reagent/medicine/syndicate_nanites/on_mob_life(mob/living/carbon/M)
M.adjustBruteLoss(-5*REM, 0) //A ton of healing - this is a 50 telecrystal investment.
@@ -985,6 +1017,7 @@
color = "#FFAF00"
metabolization_rate = 0.4 //Math is based on specific metab rate so we want this to be static AKA if define or medicine metab rate changes, we want this to stay until we can rework calculations.
overdose_threshold = 25
+ ph = 11
/datum/reagent/medicine/earthsblood/on_mob_life(mob/living/carbon/M)
if(current_cycle <= 25) //10u has to be processed before u get into THE FUN ZONE
@@ -1036,6 +1069,7 @@
reagent_state = LIQUID
color = "#27870a"
metabolization_rate = 0.4 * REAGENTS_METABOLISM
+ ph = 4.3
/datum/reagent/medicine/haloperidol/on_mob_life(mob/living/carbon/M)
for(var/datum/reagent/drug/R in M.reagents.reagent_list)
@@ -1155,6 +1189,7 @@
overdose_threshold = 20 // with the random effects this might be awesome or might kill you at less than 10u (extensively tested)
taste_description = "salt" // it actually does taste salty
var/overdose_progress = 0 // to track overdose progress
+ ph = 7.89
/datum/reagent/medicine/modafinil/on_mob_metabolize(mob/living/M)
ADD_TRAIT(M, TRAIT_SLEEPIMMUNE, type)
@@ -1217,6 +1252,7 @@
color = "#07E79E"
metabolization_rate = 0.25 * REAGENTS_METABOLISM
overdose_threshold = 30
+ ph = 9.12
/datum/reagent/medicine/psicodine/on_mob_metabolize(mob/living/L)
..()
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 32a138cf5a6298..dfeb61eb7fa4da 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -10,6 +10,7 @@
glass_desc = "Are you sure this is tomato juice?"
shot_glass_icon_state = "shotglassred"
penetrates_skin = NONE
+ ph = 7.4
/datum/reagent/blood/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message=TRUE, touch_protection=0)
. = ..()
@@ -95,6 +96,7 @@
taste_description = "gross iron"
shot_glass_icon_state = "shotglassred"
material = /datum/material/meat
+ ph = 7.45
/datum/reagent/vaccine
//data must contain virus type
@@ -217,6 +219,7 @@
glass_name = "glass of holy water"
glass_desc = "A glass of holy water."
self_consuming = TRUE //divine intervention won't be limited by the lack of a liver
+ ph = 7.5 //God is alkaline
/datum/reagent/water/holywater/on_mob_metabolize(mob/living/L)
..()
@@ -297,6 +300,7 @@
glass_name = "glass of oxygenated water"
glass_desc = "The father of all refreshments. Surely it tastes great, right?"
shot_glass_icon_state = "shotglassclear"
+ ph = 6.2
/*
* Water reaction to turf
@@ -323,6 +327,7 @@
taste_description = "suffering"
metabolization_rate = 2.5 * REAGENTS_METABOLISM //1u/tick
penetrates_skin = TOUCH|VAPOR
+ ph = 6.5
/datum/reagent/fuel/unholywater/on_mob_life(mob/living/carbon/M)
if(iscultist(M))
@@ -347,6 +352,7 @@
name = "Hell Water"
description = "YOUR FLESH! IT BURNS!"
taste_description = "burning"
+ ph = 0.1
/datum/reagent/hellwater/on_mob_life(mob/living/carbon/M)
M.set_fire_stacks(min(5, M.fire_stacks + 3))
@@ -389,6 +395,7 @@
metabolization_rate = 10 * REAGENTS_METABOLISM // very fast, so it can be applied rapidly. But this changes on an overdose
overdose_threshold = 11 //Slightly more than one un-nozzled spraybottle.
taste_description = "sour oranges"
+ ph = 5
/datum/reagent/spraytan/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE)
. = ..()
@@ -696,6 +703,7 @@
color = "#202040" // rgb: 20, 20, 40
metabolization_rate = 0.25 * REAGENTS_METABOLISM
taste_description = "bitterness"
+ ph = 10
/datum/reagent/serotrotium/on_mob_life(mob/living/carbon/M)
if(ishuman(M))
@@ -709,6 +717,7 @@
reagent_state = GAS
color = "#808080" // rgb: 128, 128, 128
taste_mult = 0 // oderless and tasteless
+ ph = 9.2//It's acutally a huge range and very dependant on the chemistry but ph is basically a made up var in it's implementation anyways
/datum/reagent/oxygen/expose_turf(turf/open/exposed_turf, reac_volume)
@@ -724,6 +733,7 @@
reagent_state = SOLID
color = "#6E3B08" // rgb: 110, 59, 8
taste_description = "metal"
+ ph = 5.5
/datum/reagent/copper/expose_obj(obj/exposed_obj, reac_volume)
. = ..()
@@ -754,6 +764,7 @@
reagent_state = GAS
color = "#808080" // rgb: 128, 128, 128
taste_mult = 0
+ ph = 0.1//Now I'm stuck in a trap of my own design. Maybe I should make -ve phes? (not 0 so I don't get div/0 errors)
/datum/reagent/potassium
name = "Potassium"
@@ -782,6 +793,7 @@
reagent_state = SOLID
color = "#BF8C00" // rgb: 191, 140, 0
taste_description = "rotten eggs"
+ ph = 4.5
/datum/reagent/carbon
name = "Carbon"
@@ -789,6 +801,7 @@
reagent_state = SOLID
color = "#1C1300" // rgb: 30, 20, 0
taste_description = "sour chalk"
+ ph = 5
/datum/reagent/carbon/expose_turf(turf/exposed_turf, reac_volume)
. = ..()
@@ -805,6 +818,7 @@
reagent_state = GAS
color = "#FFFB89" //pale yellow? let's make it light gray
taste_description = "chlorine"
+ ph = 7.4
// You're an idiot for thinking that one of the most corrosive and deadly gasses would be beneficial
@@ -829,6 +843,7 @@
reagent_state = GAS
color = "#808080" // rgb: 128, 128, 128
taste_description = "acid"
+ ph = 2
// You're an idiot for thinking that one of the most corrosive and deadly gasses would be beneficial
/datum/reagent/fluorine/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -850,6 +865,7 @@
reagent_state = SOLID
color = "#808080" // rgb: 128, 128, 128
taste_description = "salty metal"
+ ph = 11.6
/datum/reagent/phosphorus
name = "Phosphorus"
@@ -857,6 +873,7 @@
reagent_state = SOLID
color = "#832828" // rgb: 131, 40, 40
taste_description = "vinegar"
+ ph = 6.5
// Phosphoric salts are beneficial though. And even if the plant suffers, in the long run the tray gets some nutrients. The benefit isn't worth that much.
/datum/reagent/phosphorus/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -872,6 +889,7 @@
reagent_state = SOLID
color = "#808080" // rgb: 128, 128, 128
taste_description = "metal"
+ ph = 11.3
/datum/reagent/lithium/on_mob_life(mob/living/carbon/M)
if(!HAS_TRAIT(M, TRAIT_IMMOBILIZED) && !isspaceturf(M.loc))
@@ -885,12 +903,14 @@
description = "Glycerol is a simple polyol compound. Glycerol is sweet-tasting and of low toxicity."
color = "#D3B913"
taste_description = "sweetness"
+ ph = 9
/datum/reagent/space_cleaner/sterilizine
name = "Sterilizine"
description = "Sterilizes wounds in preparation for surgery."
color = "#D0EFEE" // space cleaner but lighter
taste_description = "bitterness"
+ ph = 10.5
/datum/reagent/space_cleaner/sterilizine/expose_mob(mob/living/carbon/exposed_carbon, methods=TOUCH, reac_volume)
. = ..()
@@ -909,6 +929,7 @@
material = /datum/material/iron
color = "#606060" //pure iron? let's make it violet of course
+ ph = 6
/datum/reagent/iron/on_mob_life(mob/living/carbon/C)
if(C.blood_volume < BLOOD_VOLUME_NORMAL)
@@ -938,6 +959,7 @@
color = "#5E9964" //this used to be silver, but liquid uranium can still be green and it's more easily noticeable as uranium like this so why bother?
taste_description = "the inside of a reactor"
var/irradiation_level = 1
+ ph = 4
material = /datum/material/uranium
/datum/reagent/uranium/on_mob_life(mob/living/carbon/M)
@@ -971,6 +993,7 @@
taste_description = "the colour blue and regret"
irradiation_level = 2*REM
material = null
+ ph = 10
/datum/reagent/uranium/radium/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
. = ..()
@@ -985,6 +1008,7 @@
color = "#0000CC"
taste_description = "fizzling blue"
material = /datum/material/bluespace
+ ph = 12
/datum/reagent/bluespace/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -1016,6 +1040,7 @@
color = "#A8A8A8" // rgb: 168, 168, 168
taste_mult = 0
material = /datum/material/glass
+ ph = 10
/datum/reagent/fuel
name = "Welding fuel"
@@ -1026,6 +1051,7 @@
glass_name = "glass of welder fuel"
glass_desc = "Unless you're an industrial tool, this is probably not safe for consumption."
penetrates_skin = NONE
+ ph = 4
/datum/reagent/fuel/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume)//Splashing people with welding fuel to make them easy to ignite!
. = ..()
@@ -1045,6 +1071,7 @@
reagent_weight = 0.6 //so it sprays further
penetrates_skin = NONE
var/clean_types = CLEAN_WASH
+ ph = 5.5
/datum/reagent/space_cleaner/expose_obj(obj/exposed_obj, reac_volume)
. = ..()
@@ -1076,6 +1103,7 @@
metabolization_rate = 1.5 * REAGENTS_METABOLISM
taste_description = "acid"
penetrates_skin = VAPOR
+ ph = 2
/datum/reagent/space_cleaner/ez_clean/on_mob_life(mob/living/carbon/M)
M.adjustBruteLoss(3.33)
@@ -1095,6 +1123,7 @@
color = "#ADB5DB" //i hate default violets and 'crypto' keeps making me think of cryo so it's light blue now
metabolization_rate = 1.5 * REAGENTS_METABOLISM
taste_description = "sourness"
+ ph = 11.9
/datum/reagent/cryptobiolin/on_mob_life(mob/living/carbon/M)
M.Dizzy(1)
@@ -1106,6 +1135,7 @@
description = "Impedrezene is a narcotic that impedes one's ability by slowing down the higher brain cell functions."
color = "#E07DDD" // pink = happy = dumb
taste_description = "numbness"
+ ph = 9.1
/datum/reagent/impedrezene/on_mob_life(mob/living/carbon/M)
M.jitteriness = max(M.jitteriness-5,0)
@@ -1150,6 +1180,7 @@
can_synth = FALSE
taste_description = "slime"
penetrates_skin = NONE
+ ph = 11
/datum/reagent/fungalspores/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
@@ -1163,6 +1194,7 @@
taste_description = "goo"
can_synth = FALSE //special orange man request
penetrates_skin = NONE
+ ph = 11
/datum/reagent/snail/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
@@ -1174,6 +1206,7 @@
description = "A perfluoronated sulfonic acid that forms a foam when mixed with water."
color = "#9E6B38" // rgb: 158, 107, 56
taste_description = "metal"
+ ph = 11
/datum/reagent/foaming_agent// Metal foaming agent. This is lithium hydride. Add other recipes (e.g. LiH + H2O -> LiOH + H2) eventually.
name = "Foaming agent"
@@ -1181,6 +1214,7 @@
reagent_state = SOLID
color = "#664B63" // rgb: 102, 75, 99
taste_description = "metal"
+ ph = 11.5
/datum/reagent/smart_foaming_agent //Smart foaming agent. Functions similarly to metal foam, but conforms to walls.
name = "Smart foaming agent"
@@ -1188,6 +1222,7 @@
reagent_state = SOLID
color = "#664B63" // rgb: 102, 75, 99
taste_description = "metal"
+ ph = 11.8
/datum/reagent/ammonia
name = "Ammonia"
@@ -1195,6 +1230,7 @@
reagent_state = GAS
color = "#404030" // rgb: 64, 64, 48
taste_description = "mordant"
+ ph = 11.6
/datum/reagent/ammonia/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
. = ..()
@@ -1210,6 +1246,7 @@
description = "A secondary amine, mildly corrosive."
color = "#604030" // rgb: 96, 64, 48
taste_description = "iron"
+ ph = 12
// This is more bad ass, and pests get hurt by the corrosive nature of it, not the plant. The new trade off is it culls stability.
/datum/reagent/diethylamine/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -1227,6 +1264,7 @@
description = "A gas commonly produced by burning carbon fuels. You're constantly producing this in your lungs."
color = "#B0B0B0" // rgb : 192, 192, 192
taste_description = "something unknowable"
+ ph = 6
/datum/reagent/carbondioxide/expose_turf(turf/open/exposed_turf, reac_volume)
if(istype(exposed_turf))
@@ -1241,6 +1279,7 @@
metabolization_rate = 1.5 * REAGENTS_METABOLISM
color = "#808080"
taste_description = "sweetness"
+ ph = 5.8
/datum/reagent/nitrous_oxide/expose_turf(turf/open/exposed_turf, reac_volume)
. = ..()
@@ -1270,6 +1309,7 @@
metabolization_rate = REAGENTS_METABOLISM * 0.5 // Because stimulum/nitryl/freon/hypernoblium are handled through gas breathing, metabolism must be lower for breathcode to keep up
color = "E1A116"
taste_description = "sourness"
+ ph = 1.8
/datum/reagent/stimulum/on_mob_metabolize(mob/living/L)
..()
@@ -1293,6 +1333,7 @@
metabolization_rate = REAGENTS_METABOLISM * 0.5 // Because stimulum/nitryl/freon/hypernoblium are handled through gas breathing, metabolism must be lower for breathcode to keep up
color = "90560B"
taste_description = "burning"
+ ph = 2
/datum/reagent/nitryl/on_mob_metabolize(mob/living/L)
..()
@@ -1401,18 +1442,21 @@
colorname = "red"
color = "#DA0000" // red
random_color_list = list("#FC7474")
+ ph = 0.5
/datum/reagent/colorful_reagent/powder/orange
name = "Orange Powder"
colorname = "orange"
color = "#FF9300" // orange
random_color_list = list("#FF9300")
+ ph = 2
/datum/reagent/colorful_reagent/powder/yellow
name = "Yellow Powder"
colorname = "yellow"
color = "#FFF200" // yellow
random_color_list = list("#FFF200")
+ ph = 5
/datum/reagent/colorful_reagent/powder/green
name = "Green Powder"
@@ -1425,12 +1469,14 @@
colorname = "blue"
color = "#00B7EF" // blue
random_color_list = list("#71CAE5")
+ ph = 10
/datum/reagent/colorful_reagent/powder/purple
name = "Purple Powder"
colorname = "purple"
color = "#DA00FF" // purple
random_color_list = list("#BD8FC4")
+ ph = 13
/datum/reagent/colorful_reagent/powder/invisible
name = "Invisible Powder"
@@ -1494,6 +1540,7 @@
color = "#000000" // RBG: 0, 0, 0
var/tox_prob = 0
taste_description = "plant food"
+ ph = 3
/datum/reagent/plantnutriment/on_mob_life(mob/living/carbon/M)
if(prob(tox_prob))
@@ -1583,6 +1630,7 @@
color = "#2D2D2D"
taste_description = "bitterness"
taste_mult = 1.5
+ ph = 1.5
/datum/reagent/stable_plasma/on_mob_life(mob/living/carbon/C)
C.adjustPlasma(10)
@@ -1594,6 +1642,7 @@
reagent_state = LIQUID
color = "#BC8A00"
taste_description = "metal"
+ ph = 4.5
/datum/reagent/carpet
name = "Carpet"
@@ -1698,6 +1747,7 @@
reagent_state = LIQUID
color = "#D35415"
taste_description = "chemicals"
+ ph = 7.8
/datum/reagent/pentaerythritol
name = "Pentaerythritol"
@@ -1735,6 +1785,7 @@
reagent_state = LIQUID
color = "#E7EA91"
taste_description = "acid"
+ ph = 5.5
/datum/reagent/ash
name = "Ash"
@@ -1742,6 +1793,7 @@
reagent_state = LIQUID
color = "#515151"
taste_description = "ash"
+ ph = 6.5
// Ash is also used IRL in gardening, as a fertilizer enhancer and weed killer
/datum/reagent/ash/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -1875,6 +1927,7 @@
reagent_state = LIQUID
color = "#60A584" // rgb: 96, 165, 132
taste_description = "cool salt"
+ ph = 11.2
// Saltpetre is used for gardening IRL, to simplify highly, it speeds up growth and strengthens plants
/datum/reagent/saltpetre/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -1892,6 +1945,7 @@
reagent_state = LIQUID
color = "#FFFFD6" // very very light yellow
taste_description = "acid"
+ ph = 11.9
/datum/reagent/drying_agent
name = "Drying agent"
@@ -1899,6 +1953,7 @@
reagent_state = LIQUID
color = "#A70FFF"
taste_description = "dryness"
+ ph = 10.7
/datum/reagent/drying_agent/expose_turf(turf/open/exposed_turf, reac_volume)
. = ..()
@@ -1965,6 +2020,7 @@
description = "Royal Bee Jelly, if injected into a Queen Space Bee said bee will split into two bees."
color = "#00ff80"
taste_description = "strange honey"
+ ph = 3
/datum/reagent/royal_bee_jelly/on_mob_life(mob/living/carbon/M)
if(prob(2))
@@ -1985,6 +2041,7 @@
metabolization_rate = INFINITY
can_synth = FALSE
taste_description = "brains"
+ ph = 0.5
/datum/reagent/romerol/expose_mob(mob/living/carbon/human/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -2041,6 +2098,7 @@
description = "the petroleum based components of plastic."
color = "#f7eded"
taste_description = "plastic"
+ ph = 6
/datum/reagent/glitter
name = "generic glitter"
@@ -2079,6 +2137,7 @@
color = "#AAAAAA55"
taste_description = "water"
metabolization_rate = 0.25 * REAGENTS_METABOLISM
+ ph = 15
/datum/reagent/pax/on_mob_metabolize(mob/living/L)
..()
@@ -2181,6 +2240,7 @@
var/yuck_cycle = 0 //! The `current_cycle` when puking starts.
/datum/reagent/yuck/on_mob_add(mob/living/L)
+ . = ..()
if(HAS_TRAIT(L, TRAIT_NOHUNGER)) //they can't puke
holder.del_reagent(type)
@@ -2299,6 +2359,7 @@
/datum/reagent/gravitum/on_mob_add(mob/living/L)
L.AddElement(/datum/element/forced_gravity, 0) //0 is the gravity, and in this case weightless
+ return ..()
/datum/reagent/gravitum/on_mob_end_metabolize(mob/living/L)
L.RemoveElement(/datum/element/forced_gravity, 0)
@@ -2371,3 +2432,4 @@
M.adjustOxyLoss(2, FALSE)
M.adjustBruteLoss(2, FALSE)
..()
+
diff --git a/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm b/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm
new file mode 100644
index 00000000000000..26aa514e7acb68
--- /dev/null
+++ b/code/modules/reagents/chemistry/reagents/reaction_agents_reagents.dm
@@ -0,0 +1,124 @@
+/datum/reagent/reaction_agent
+ name = "reaction agent"
+ description = "Hello! I am a bugged reagent. Please report me for my crimes. Thank you!!"
+
+/datum/reagent/reaction_agent/intercept_reagents_transfer(datum/reagents/target, amount)
+ if(!target)
+ return FALSE
+ if(target.flags & NO_REACT)
+ return FALSE
+ if(target.has_reagent(/datum/reagent/stabilizing_agent))
+ return FALSE
+ if(LAZYLEN(target.reagent_list) == 0)
+ return FALSE
+ if(LAZYLEN(target.reagent_list) == 1)
+ if(target.has_reagent(type)) //Allow dispensing into self
+ return FALSE
+ return TRUE
+
+/datum/reagent/reaction_agent/acidic_buffer
+ name = "Strong acidic buffer"
+ description = "This reagent will consume itself and move the pH of a beaker towards acidity when added to another."
+ color = "#fbc314"
+ ph = 0
+ ///The strength of the buffer where (volume/holder.total_volume)*strength. So for 1u added to 50u the ph will decrease by 0.5
+ var/strength = 30
+
+//Consumes self on addition and shifts ph
+/datum/reagent/reaction_agent/acidic_buffer/intercept_reagents_transfer(datum/reagents/target, amount)
+ . = ..()
+ if(!.)
+ return
+ if(target.ph <= ph)
+ target.my_atom.audible_message("The beaker froths as the buffer is added, to no effect.")
+ playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
+ holder.remove_reagent(type, amount)//Remove from holder because it's not transfered
+ return
+ var/ph_change = -((amount/target.total_volume)*strength)
+ target.adjust_all_reagents_ph(ph_change, ph, 14)
+ target.my_atom.audible_message("The beaker fizzes as the ph changes!")
+ playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
+ holder.remove_reagent(type, amount)
+
+/datum/reagent/reaction_agent/basic_buffer
+ name = "Strong basic buffer"
+ description = "This reagent will consume itself and move the pH of a beaker towards alkalinity when added to another."
+ color = "#3853a4"
+ ph = 14
+ ///The strength of the buffer where (volume/holder.total_volume)*strength. So for 1u added to 50u the ph will increase by 0.5
+ var/strength = 30
+
+/datum/reagent/reaction_agent/basic_buffer/intercept_reagents_transfer(datum/reagents/target, amount)
+ . = ..()
+ if(!.)
+ return
+ if(target.ph >= ph)
+ target.my_atom.audible_message("The beaker froths as the buffer is added, to no effect.")
+ playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
+ holder.remove_reagent(type, amount)//Remove from holder because it's not transfered
+ return
+ var/ph_change = (amount/target.total_volume)*strength
+ target.adjust_all_reagents_ph(ph_change, 0, ph)
+ target.my_atom.audible_message("The beaker froths as the ph changes!")
+ playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
+ holder.remove_reagent(type, amount)
+
+//purity testor/reaction agent prefactors
+
+/datum/reagent/prefactor_a
+ name = "Interim product alpha"
+ description = "This reagent is a prefactor to the purity tester reagent, and will react with stable plasma to create it"
+ color = "#bafa69"
+
+/datum/reagent/prefactor_b
+ name = "Interim product beta"
+ description = "This reagent is a prefactor to the reaction speed agent reagent, and will react with stable plasma to create it"
+ color = "#8a3aa9"
+
+/datum/reagent/reaction_agent/purity_tester
+ name = "Purity tester"
+ description = "This reagent will consume itself and violently react if there is a highly impure reagent in the beaker."
+ ph = 3
+ color = "#ffffff"
+
+/datum/reagent/reaction_agent/purity_tester/intercept_reagents_transfer(datum/reagents/target, amount)
+ . = ..()
+ if(!.)
+ return
+ var/is_inverse = FALSE
+ for(var/_reagent in target.reagent_list)
+ var/datum/reagent/reaction_agent/reagent = _reagent
+ if(reagent.purity <= reagent.inverse_chem_val)
+ is_inverse = TRUE
+ if(is_inverse)
+ target.my_atom.audible_message("The beaker bubbles violently as the reagent is added!")
+ playsound(target.my_atom, 'sound/chemistry/bufferadd.ogg', 50, TRUE)
+ else
+ target.my_atom.audible_message("The added reagent doesn't seem to do much.")
+ holder.remove_reagent(type, amount)
+
+/datum/reagent/reaction_agent/speed_agent
+ name = "Tempomyocin"
+ description = "This reagent will consume itself and speed up an ongoing reaction, modifying the current reaction's purity by it's own."
+ ph = 10
+ color = "#e61f82"
+ ///How much the reaction speed is sped up by - for 5u added to 100u, an additional step of 1 will be done up to a max of 2x
+ var/strength = 20
+
+
+/datum/reagent/reaction_agent/speed_agent/intercept_reagents_transfer(datum/reagents/target, amount)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!length(target.reaction_list))//you can add this reagent to a beaker with no ongoing reactions, so this prevents it from being used up.
+ return FALSE
+ amount /= target.reaction_list.len
+ for(var/_reaction in target.reaction_list)
+ var/datum/equilibrium/reaction = _reaction
+ if(!reaction)
+ CRASH("[_reaction] is in the reaction list, but is not an equilibrium")
+ var/power = (amount/reaction.target_vol)*strength
+ power *= creation_purity
+ power = clamp(power, 0, 2)
+ reaction.react_timestep(power, creation_purity)
+ holder.remove_reagent(type, amount)
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 0e480cd999571a..1f5fc5b6c9f75e 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -29,6 +29,7 @@
color = "#792300" // rgb: 121, 35, 0
toxpwr = 2.5
taste_description = "mushroom"
+ ph = 13
/datum/reagent/toxin/mutagen
name = "Unstable mutagen"
@@ -37,6 +38,7 @@
toxpwr = 0
taste_description = "slime"
taste_mult = 0.9
+ ph = 2.3
/datum/reagent/toxin/mutagen/expose_mob(mob/living/carbon/exposed_mob, methods=TOUCH, reac_volume)
. = ..()
@@ -72,6 +74,7 @@
toxpwr = 3
material = /datum/material/plasma
penetrates_skin = NONE
+ ph = 4
/datum/reagent/toxin/plasma/on_new(data)
. = ..()
@@ -139,6 +142,7 @@
color = "#7DC3A0"
toxpwr = 0
taste_description = "acid"
+ ph = 1.2
/datum/reagent/toxin/lexorin/on_mob_life(mob/living/carbon/C)
. = TRUE
@@ -160,6 +164,7 @@
toxpwr = 0
taste_description = "slime"
taste_mult = 1.3
+ ph = 10
/datum/reagent/toxin/slimejelly/on_mob_life(mob/living/carbon/M)
if(prob(10))
@@ -177,6 +182,7 @@
color = "#CF3600" // rgb: 207, 54, 0
toxpwr = 0
taste_description = "mint"
+ ph = 8
/datum/reagent/toxin/minttoxin/on_mob_life(mob/living/carbon/M)
if(HAS_TRAIT(M, TRAIT_FAT))
@@ -190,6 +196,7 @@
color = "#003333" // rgb: 0, 51, 51
toxpwr = 2
taste_description = "fish"
+ ph = 12
/datum/reagent/toxin/carpotoxin/on_mob_life(mob/living/carbon/M)
. = ..()
@@ -206,6 +213,7 @@
taste_description = "death"
penetrates_skin = NONE
var/fakedeath_active = FALSE
+ ph = 13
/datum/reagent/toxin/zombiepowder/on_mob_metabolize(mob/living/L)
..()
@@ -247,6 +255,7 @@
color = "#664700" // rgb: 102, 71, 0
toxpwr = 0.8
taste_description = "death"
+ ph = 14.5
/datum/reagent/toxin/ghoulpowder/on_mob_metabolize(mob/living/L)
..()
@@ -267,6 +276,7 @@
color = "#B31008" // rgb: 139, 166, 233
toxpwr = 0
taste_description = "sourness"
+ ph = 11
/datum/reagent/toxin/mindbreaker/on_mob_life(mob/living/carbon/M)
M.hallucination += 5
@@ -279,6 +289,7 @@
toxpwr = 1
taste_mult = 1
penetrates_skin = NONE
+ ph = 2.7
// Plant-B-Gone is just as bad
/datum/reagent/toxin/plantbgone/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -311,6 +322,7 @@
name = "Weed Killer"
description = "A harmful toxic mixture to kill weeds. Do not ingest!"
color = "#4B004B" // rgb: 75, 0, 75
+ ph = 3
//Weed Spray
/datum/reagent/toxin/plantbgone/weedkiller/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -325,6 +337,7 @@
description = "A harmful toxic mixture to kill pests. Do not ingest!"
color = "#4B004B" // rgb: 75, 0, 75
toxpwr = 1
+ ph = 3.2
//Pest Spray
/datum/reagent/toxin/pestkiller/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -359,6 +372,7 @@
description = "A natural toxin produced by blob spores that inhibits vision when ingested."
color = "#9ACD32"
toxpwr = 1
+ ph = 11
/datum/reagent/toxin/spore/on_mob_life(mob/living/carbon/C)
C.damageoverlaytemp = 60
@@ -372,6 +386,7 @@
color = "#9ACD32"
toxpwr = 0.5
taste_description = "burning"
+ ph = 13
/datum/reagent/toxin/spore_burning/on_mob_life(mob/living/carbon/M)
M.adjust_fire_stacks(2)
@@ -386,6 +401,7 @@
color = "#000067" // rgb: 0, 0, 103
toxpwr = 0
metabolization_rate = 1.5 * REAGENTS_METABOLISM
+ ph = 11
/datum/reagent/toxin/chloralhydrate/on_mob_life(mob/living/carbon/M)
switch(current_cycle)
@@ -410,6 +426,7 @@
glass_icon_state = "beerglass"
glass_name = "glass of beer"
glass_desc = "A freezing pint of beer."
+ ph = 2
/datum/reagent/toxin/fakebeer/on_mob_life(mob/living/carbon/M)
switch(current_cycle)
@@ -426,6 +443,7 @@
reagent_state = SOLID
color = "#5B2E0D" // rgb: 91, 46, 13
toxpwr = 0.5
+ ph = 4.2
/datum/reagent/toxin/teapowder
name = "Ground Tea Leaves"
@@ -434,6 +452,7 @@
color = "#7F8400" // rgb: 127, 132, 0
toxpwr = 0.1
taste_description = "green tea"
+ ph = 4.9
/datum/reagent/toxin/mutetoxin //the new zombie powder.
name = "Mute Toxin"
@@ -442,6 +461,7 @@
color = "#F0F8FF" // rgb: 240, 248, 255
toxpwr = 0
taste_description = "silence"
+ ph = 12.2
/datum/reagent/toxin/mutetoxin/on_mob_life(mob/living/carbon/M)
M.silent = max(M.silent, 3)
@@ -850,6 +870,7 @@
var/acidpwr = 10 //the amount of protection removed from the armour
taste_description = "acid"
self_consuming = TRUE
+ ph = 2.75
// ...Why? I mean, clearly someone had to have done this and thought, well, acid doesn't hurt plants, but what brought us here, to this point?
/datum/reagent/toxin/acid/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -892,6 +913,7 @@
color = "#5050FF"
toxpwr = 2
acidpwr = 42.0
+ ph = 1.3
// SERIOUSLY
/datum/reagent/toxin/acid/fluacid/on_hydroponics_apply(obj/item/seeds/myseed, datum/reagents/chems, obj/machinery/hydroponics/mytray, mob/user)
@@ -912,6 +934,7 @@
color = "#5050FF"
toxpwr = 3
acidpwr = 5.0
+ ph = 3.3
/datum/reagent/toxin/acid/nitracid/on_mob_life(mob/living/carbon/M)
M.adjustFireLoss(volume/10, FALSE) //here you go nervar
@@ -962,6 +985,7 @@
/datum/reagent/toxin/bonehurtingjuice/on_mob_add(mob/living/carbon/M)
M.say("oof ouch my bones", forced = /datum/reagent/toxin/bonehurtingjuice)
+ return ..()
/datum/reagent/toxin/bonehurtingjuice/on_mob_life(mob/living/carbon/M)
M.adjustStaminaLoss(7.5, 0)
diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm
index a8e0074c0c8849..13fe4af901d79b 100644
--- a/code/modules/reagents/chemistry/recipes.dm
+++ b/code/modules/reagents/chemistry/recipes.dm
@@ -21,15 +21,42 @@
///Determines if a chemical reaction can occur inside a mob
var/mob_react = TRUE
- ///Required temperature for the reaction to begin
- var/required_temp = 0
- /// Set to TRUE if you want the recipe to only react when it's BELOW the required temp.
- var/is_cold_recipe = FALSE
+
///The message shown to nearby people upon mixing, if applicable
var/mix_message = "The solution begins to bubble."
///The sound played upon mixing, if applicable
var/mix_sound = 'sound/effects/bubbles.ogg'
+ /// Set to TRUE if you want the recipe to only react when it's BELOW the required temp.
+ var/is_cold_recipe = FALSE
+ ///FermiChem! - See fermi_readme.md
+ ///Required temperature for the reaction to begin, for fermimechanics it defines the lower area of bell curve for determining heat based rate reactions, aka the minimum
+ var/required_temp = 100
+ /// Upper end for above (i.e. the end of the curve section defined by temp_exponent_factor)
+ var/optimal_temp = 500
+ /// Temperature at which reaction explodes - If any reaction is this hot, it explodes!
+ var/overheat_temp = 900
+ /// Lowest value of pH determining pH a 1 value for pH based rate reactions (Plateu phase)
+ var/optimal_ph_min = 5
+ /// Higest value for above
+ var/optimal_ph_max = 9
+ /// How far out pH wil react, giving impurity place (Exponential phase)
+ var/determin_ph_range = 4
+ /// How sharp the temperature exponential curve is (to the power of value)
+ var/temp_exponent_factor = 2
+ /// How sharp the pH exponential curve is (to the power of value)
+ var/ph_exponent_factor = 2
+ /// How much the temperature will change (with no intervention) (i.e. for 30u made the temperature will increase by 100, same with 300u. The final temp will always be start + this value, with the exception con beakers with different specific heats)
+ var/thermic_constant = 50
+ /// pH change per 1u reaction
+ var/H_ion_release = 0.01
+ /// Optimal/max rate possible if all conditions are perfect
+ var/rate_up_lim = 30
+ /// If purity is below 0.15, it calls OverlyImpure() too. Set to 0 to disable this.
+ var/purity_min = 0.15
+ /// bitflags for clear conversions; REACTION_CLEAR_IMPURE, REACTION_CLEAR_INVERSE, REACTION_CLEAR_RETAIN, REACTION_INSTANT
+ var/reaction_flags = NONE
+
/datum/chemical_reaction/New()
. = ..()
SSticker.OnRoundstart(CALLBACK(src,.proc/update_info))
@@ -44,8 +71,14 @@
/datum/chemical_reaction/proc/update_info()
return
+///REACTION PROCS
+
/**
* Shit that happens on reaction
+ * Only procs at the START of a reaction
+ * use reaction_step() for each step of a reaction
+ * or reaction_end() when the reaction stops
+ * If reaction_flags & REACTION_INSTANT then this is the only proc that is called.
*
* Proc where the additional magic happens.
* You dont want to handle mob spawning in this since there is a dedicated proc for that.client
@@ -53,10 +86,131 @@
* * holder - the datum that holds this reagent, be it a beaker or anything else
* * created_volume - volume created when this is mixed. look at 'var/list/results'.
*/
-/datum/chemical_reaction/proc/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/proc/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
return
//I recommend you set the result amount to the total volume of all components.
+/**
+ * Stuff that occurs in the middle of a reaction
+ * Only procs DURING a reaction
+ * If reaction_flags & REACTION_INSTANT then this isn't called
+ * returning END_REACTION will END the reaction
+ *
+ * Arguments:
+ * * reaction - the equilibrium reaction holder that is reaction is processed within - use this to edit delta_t and delta
+ * * holder - the datum that holds this reagent, be it a beaker or anything else
+ * * created_volume - volume created per step
+ * * added_purity - how pure the created volume is per step
+ *
+ * Outputs:
+ * * returning END_REACTION will end the associated reaction - flagging it for deletion and preventing any reaction in that timestep from happening. Make sure to set the vars in the holder to one that can't start it from starting up again.
+ */
+/datum/chemical_reaction/proc/reaction_step(datum/equilibrium/reaction, datum/reagents/holder, delta_t, delta_ph, step_reaction_vol)
+ return
+
+/**
+ * Stuff that occurs at the end of a reaction. This will proc if the beaker is forced to stop and start again (say for sudden temperature changes).
+ * Only procs at the END of reaction
+ * If reaction_flags & REACTION_INSTANT then this isn't called
+ * if reaction_flags REACTION_CLEAR_IMPURE then the impurity chem is handled here, producing the result in the beaker instead of in a mob
+ * Likewise for REACTION_CLEAR_INVERSE the inverse chem is produced at the end of the reaction in the beaker
+ * You should be calling ..() if you're writing a child function of this proc otherwise purity methods won't work correctly
+ *
+ * Proc where the additional magic happens.
+ * You dont want to handle mob spawning in this since there is a dedicated proc for that.client
+ * Arguments:
+ * * holder - the datum that holds this reagent, be it a beaker or anything else
+ * * react_volume - volume created across the whole reaction
+ */
+/datum/chemical_reaction/proc/reaction_finish(datum/reagents/holder, react_vol)
+ //failed_chem handler
+ var/cached_temp = holder.chem_temp
+ for(var/id in results)
+ var/datum/reagent/reagent = holder.has_reagent(id)
+ if(!reagent)
+ continue
+ //Split like this so it's easier for people to edit this function in a child
+ convert_into_failed(reagent, holder)
+ reaction_clear_check(reagent, holder)
+ holder.chem_temp = cached_temp
+
+/**
+ * Converts a reagent into the type specified by the failed_chem var of the input reagent
+ *
+ * Arguments:
+ * * reagent - the target reagent to convert
+ */
+/datum/chemical_reaction/proc/convert_into_failed(datum/reagent/reagent, datum/reagents/holder)
+ if(reagent.purity < purity_min)
+ var/cached_volume = reagent.volume
+ holder.remove_reagent(reagent.type, cached_volume, FALSE)
+ holder.add_reagent(reagent.failed_chem, cached_volume, FALSE, added_purity = 1)
+ SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[type] failed reactions")
+
+/**
+ * REACTION_CLEAR handler
+ * If the reaction has the REACTION_CLEAR flag, then it will split using purity methods in the beaker instead
+ *
+ * Arguments:
+ * * reagent - the target reagent to convert
+ */
+/datum/chemical_reaction/proc/reaction_clear_check(datum/reagent/reagent, datum/reagents/holder)
+ if(!reagent)//Failures can delete R
+ return
+ if(reaction_flags & (REACTION_CLEAR_IMPURE | REACTION_CLEAR_INVERSE))
+ if(reagent.purity == 1)
+ return
+
+ var/cached_volume = reagent.volume
+ if((reaction_flags & REACTION_CLEAR_INVERSE) && reagent.inverse_chem)
+ if(reagent.inverse_chem_val > reagent.purity)
+ holder.remove_reagent(reagent.type, cached_volume, FALSE)
+ holder.add_reagent(reagent.inverse_chem, cached_volume, FALSE, added_purity = 1)
+
+ if((reaction_flags & REACTION_CLEAR_IMPURE) && reagent.impure_chem)
+ var/impureVol = cached_volume * (1 - reagent.purity)
+ holder.remove_reagent(reagent.type, (impureVol), FALSE)
+ holder.add_reagent(reagent.impure_chem, impureVol, FALSE, added_purity = 1)
+ reagent.creation_purity = reagent.purity
+ reagent.purity = 1
+
+/**
+ * Occurs when a reation is overheated (i.e. past it's overheatTemp)
+ * Will be called every tick in the reaction that it is overheated
+ * If you want this to be a once only proc (i.e. the reaction is stopped after) set reaction.toDelete = TRUE
+ * The above is useful if you're writing an explosion
+ * By default the parent proc will reduce the final yield slightly. If you don't want that don't add ..()
+ *
+ * Arguments:
+ * * holder - the datum that holds this reagent, be it a beaker or anything else
+ * * equilibrium - the equilibrium datum that contains the equilibrium reaction properties and methods
+ */
+/datum/chemical_reaction/proc/overheated(datum/reagents/holder, datum/equilibrium/equilibrium)
+ for(var/id in results)
+ var/datum/reagent/reagent = holder.get_reagent(id)
+ if(!reagent)
+ return
+ reagent.volume = round((reagent.volume*0.98), 0.01) //Slowly lower yield per tick
+
+/**
+ * Occurs when a reation is too impure (i.e. it's below purity_min)
+ * Will be called every tick in the reaction that it is too impure
+ * If you want this to be a once only proc (i.e. the reaction is stopped after) set reaction.toDelete = TRUE
+ * The above is useful if you're writing an explosion
+ * By default the parent proc will reduce the purity of all reagents involved in the reaction in the beaker slightly. If you don't want that don't add ..()
+ *
+ * Arguments:
+ * * holder - the datum that holds this reagent, be it a beaker or anything else
+ * * equilibrium - the equilibrium datum that contains the equilibrium reaction properties and methods
+ */
+/datum/chemical_reaction/proc/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium)
+ var/affected_list = results + required_reagents
+ for(var/_reagent in affected_list)
+ var/datum/reagent/reagent = holder.get_reagent(_reagent)
+ if(!reagent)
+ continue
+ reagent.purity = clamp((reagent.purity-0.01), 0, 1) //slowly reduce purity of reagents
+
/**
* Magical mob spawning when chemicals react
*
@@ -132,3 +286,107 @@
else
if(step_towards(X, T) && moving_power > 1)
addtimer(CALLBACK(GLOBAL_PROC, .proc/_step_towards, X, T), 2)
+
+//////////////////Generic explosions/failures////////////////////
+// It is HIGHLY, HIGHLY recomended that you consume all/a good volume of the reagents/products in an explosion - because it will just keep going forever until the reaction stops
+//If you have competitive reactions - it's a good idea to consume ALL reagents in a beaker (or product+reactant), otherwise it'll swing back with the deficit and blow up again
+
+
+//Spews out the inverse of the chems in the beaker of the products/reactants only
+/datum/chemical_reaction/proc/explode_invert_smoke(datum/reagents/holder, datum/equilibrium/equilibrium, clear_products = TRUE, clear_reactants = TRUE)
+ var/datum/reagents/invert_reagents = new (2100, NO_REACT)//I think the biggest size we can get is 2100?
+ var/datum/effect_system/smoke_spread/chem/smoke = new()
+ var/sum_volume = 0
+ invert_reagents.my_atom = holder.my_atom //Give the gas a fingerprint
+ for(var/datum/reagent/reagent as anything in holder.reagent_list) //make gas for reagents, has to be done this way, otherwise it never stops Exploding
+ if(!(reagent.type in required_reagents) || !(reagent.type in results))
+ continue
+ if(reagent.inverse_chem)
+ invert_reagents.add_reagent(reagent.inverse_chem, reagent.volume, no_react = TRUE)
+ holder.remove_reagent(reagent.type, reagent.volume)
+ continue
+ invert_reagents.add_reagent(reagent.type, reagent.volume, added_purity = reagent.purity, no_react = TRUE)
+ sum_volume += reagent.volume
+ holder.remove_reagent(reagent.type, reagent.volume)
+ if(invert_reagents.reagent_list)
+ smoke.set_up(invert_reagents, (sum_volume/5), holder.my_atom)
+ smoke.start()
+ holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
+ if(clear_reactants)
+ clear_reactants(holder)
+ if(clear_products)
+ clear_products(holder)
+
+//Spews out the corrisponding reactions reagents (products/required) of the beaker in a smokecloud. Doesn't spew catalysts
+/datum/chemical_reaction/proc/explode_smoke(datum/reagents/holder, datum/equilibrium/equilibrium, clear_products = TRUE, clear_reactants = TRUE)
+ var/datum/reagents/reagents = new/datum/reagents(2100, NO_REACT)//Lets be safe first
+ var/datum/effect_system/smoke_spread/chem/smoke = new()
+ reagents.my_atom = holder.my_atom //fingerprint
+ var/sum_volume = 0
+ for (var/datum/reagent/reagent as anything in holder.reagent_list)
+ if((reagent.type in required_reagents) || (reagent.type in results))
+ reagents.add_reagent(reagent.type, reagent.volume, added_purity = reagent.purity, no_react = TRUE)
+ holder.remove_reagent(reagent.type, reagent.volume)
+ if(reagents.reagent_list)
+ smoke.set_up(reagents, (sum_volume/5), holder.my_atom)
+ smoke.start()
+ holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
+ if(clear_reactants)
+ clear_reactants(holder)
+ if(clear_products)
+ clear_products(holder)
+
+//Pushes everything out, and damages mobs with 10 brute damage.
+/datum/chemical_reaction/proc/explode_shockwave(datum/reagents/holder, datum/equilibrium/equilibrium)
+ var/turf/this_turf = get_turf(holder.my_atom)
+ holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, sending a shockwave rippling through the air!")
+ playsound(this_turf, 'sound/chemistry/shockwave_explosion.ogg', 80, TRUE)
+ //Modified goonvortex
+ for(var/atom/movable/movey in orange(3, this_turf))
+ if(isliving(movey))
+ var/mob/living/live = movey
+ live.apply_damage(5)//Since this can be called multiple times
+ if(movey.anchored)
+ continue
+ if(iseffect(movey) || iscameramob(movey) || isdead(movey))
+ continue
+ var/distance = get_dist(movey, this_turf)
+ var/moving_power = max(4 - distance, 1)//Make sure we're thrown out of range of the next one
+ var/atom/throw_target = get_edge_target_turf(movey, get_dir(movey, get_step_away(movey, this_turf)))
+ movey.throw_at(throw_target, moving_power, 1)
+
+
+//Creates a ring of fire in a set range around the beaker location
+/datum/chemical_reaction/proc/explode_fire(datum/reagents/holder, datum/equilibrium/equilibrium, range)
+ explosion(holder.my_atom, 0, 0, 0, 0, flame_range = 3)
+ holder.my_atom.audible_message("The [holder.my_atom] suddenly errupts in flames!")
+
+//Clears the beaker of the reagents only
+/datum/chemical_reaction/proc/clear_reactants(datum/reagents/holder, volume = null)
+ if(!holder)
+ return FALSE
+ for(var/datum/reagent/reagent as anything in holder.reagent_list)
+ if(!(reagent.type in required_reagents))
+ continue
+ if(!volume)
+ holder.remove_reagent(reagent.type, reagent.volume)
+ else
+ holder.remove_reagent(reagent.type, volume)
+
+//Clears the beaker of the product only
+/datum/chemical_reaction/proc/clear_products(datum/reagents/holder, volume = null)
+ if(!holder)
+ return FALSE
+ for(var/datum/reagent/reagent as anything in holder.reagent_list)
+ if(!(reagent.type in results))
+ continue
+ if(!volume)
+ holder.remove_reagent(reagent.type, reagent.volume)
+ else
+ holder.remove_reagent(reagent.type, volume)
+
+//Clears the beaker of ALL reagents inside
+/datum/chemical_reaction/proc/clear_reagents(datum/reagents/holder, volume = null)
+ if(!holder)
+ return FALSE
+ holder.remove_all(volume)
diff --git a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
index c266828f1e1e05..a707915787cb22 100644
--- a/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
+++ b/code/modules/reagents/chemistry/recipes/cat2_medicines.dm
@@ -1,65 +1,67 @@
-
/*****BRUTE*****/
-/datum/chemical_reaction/helbital
+/datum/chemical_reaction/medicine/helbital
results = list(/datum/reagent/medicine/c2/helbital = 3)
required_reagents = list(/datum/reagent/consumable/sugar = 1, /datum/reagent/fluorine = 1, /datum/reagent/carbon = 1)
mix_message = "The mixture turns into a thick, yellow powder."
-/datum/chemical_reaction/libital
+/datum/chemical_reaction/medicine/libital
results = list(/datum/reagent/medicine/c2/libital = 3)
required_reagents = list(/datum/reagent/phenol = 1, /datum/reagent/oxygen = 1, /datum/reagent/nitrogen = 1)
-/datum/chemical_reaction/probital
+/datum/chemical_reaction/medicine/probital
results = list(/datum/reagent/medicine/c2/probital = 4)
required_reagents = list(/datum/reagent/copper = 1, /datum/reagent/acetone = 2, /datum/reagent/phosphorus = 1)
/*****BURN*****/
-/datum/chemical_reaction/lenturi
+/datum/chemical_reaction/medicine/lenturi
results = list(/datum/reagent/medicine/c2/lenturi = 5)
required_reagents = list(/datum/reagent/ammonia = 1, /datum/reagent/silver = 1, /datum/reagent/sulfur = 1, /datum/reagent/oxygen = 1, /datum/reagent/chlorine = 1)
-/datum/chemical_reaction/aiuri
+/datum/chemical_reaction/medicine/aiuri
results = list(/datum/reagent/medicine/c2/aiuri = 4)
required_reagents = list(/datum/reagent/ammonia = 1, /datum/reagent/toxin/acid = 1, /datum/reagent/hydrogen = 2)
-/datum/chemical_reaction/hercuri
+/datum/chemical_reaction/medicine/hercuri
results = list(/datum/reagent/medicine/c2/hercuri = 5)
required_reagents = list(/datum/reagent/cryostylane = 3, /datum/reagent/bromine = 1, /datum/reagent/lye = 1)
required_temp = 47
is_cold_recipe = TRUE
+ optimal_temp = 10
+ overheat_temp = 5
+ thermic_constant = -50
/*****OXY*****/
-/datum/chemical_reaction/convermol
+/datum/chemical_reaction/medicine/convermol
results = list(/datum/reagent/medicine/c2/convermol = 3)
required_reagents = list(/datum/reagent/hydrogen = 1, /datum/reagent/fluorine = 1, /datum/reagent/fuel/oil = 1)
required_temp = 370
mix_message = "The mixture rapidly turns into a dense pink liquid."
-/datum/chemical_reaction/tirimol
+/datum/chemical_reaction/medicine/tirimol
results = list(/datum/reagent/medicine/c2/tirimol = 5)
required_reagents = list(/datum/reagent/nitrogen = 3, /datum/reagent/acetone = 2)
required_catalysts = list(/datum/reagent/toxin/acid = 1)
/*****TOX*****/
-/datum/chemical_reaction/seiver
+/datum/chemical_reaction/medicine/seiver
results = list(/datum/reagent/medicine/c2/seiver = 3)
required_reagents = list(/datum/reagent/nitrogen = 1, /datum/reagent/potassium = 1, /datum/reagent/aluminium = 1)
-/datum/chemical_reaction/multiver
+/datum/chemical_reaction/medicine/multiver
results = list(/datum/reagent/medicine/c2/multiver = 2)
required_reagents = list(/datum/reagent/ash = 1, /datum/reagent/consumable/salt = 1)
mix_message = "The mixture yields a fine black powder."
required_temp = 380
-/datum/chemical_reaction/syriniver
+/datum/chemical_reaction/medicine/syriniver
results = list(/datum/reagent/medicine/c2/syriniver = 5)
required_reagents = list(/datum/reagent/sulfur = 1, /datum/reagent/fluorine = 1, /datum/reagent/toxin = 1, /datum/reagent/nitrous_oxide = 2)
-/datum/chemical_reaction/penthrite
+/datum/chemical_reaction/medicine/penthrite
results = list(/datum/reagent/medicine/c2/penthrite = 3)
required_reagents = list(/datum/reagent/pentaerythritol = 1, /datum/reagent/acetone = 1, /datum/reagent/toxin/acid/nitracid = 1 , /datum/reagent/wittel = 1)
diff --git a/code/modules/reagents/chemistry/recipes/catalysts.dm b/code/modules/reagents/chemistry/recipes/catalysts.dm
new file mode 100644
index 00000000000000..7f723dfecb2d0b
--- /dev/null
+++ b/code/modules/reagents/chemistry/recipes/catalysts.dm
@@ -0,0 +1,26 @@
+
+///////////////////////////MEDICINES////////////////////////////
+
+/datum/chemical_reaction/medical_speed_catalyst
+ results = list(/datum/reagent/catalyst_agent/speed/medicine = 2)
+ required_reagents = list(/datum/reagent/medicine/c2/libital = 3, /datum/reagent/medicine/c2/probital = 4, /datum/reagent/toxin/plasma = 2)
+ mix_message = "The reaction evaporates slightly as the mixture solidifies"
+ mix_sound = 'sound/chemistry/catalyst.ogg'
+ required_temp = 320
+ optimal_temp = 600
+ overheat_temp = 800
+ optimal_ph_min = 5
+ optimal_ph_max = 6
+ determin_ph_range = 5
+ temp_exponent_factor = 0.5
+ ph_exponent_factor = 4
+ thermic_constant = 1000
+ H_ion_release = -0.25
+ rate_up_lim = 1
+ purity_min = 0
+
+/datum/chemical_reaction/medical_speed_catalyst/overheated(datum/reagents/holder, datum/equilibrium/equilibrium)
+ explode_invert_smoke(holder, equilibrium) //Will be better when the inputs have proper invert chems
+
+/datum/chemical_reaction/medical_speed_catalyst/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium)
+ explode_invert_smoke(holder, equilibrium)
diff --git a/code/modules/reagents/chemistry/recipes/drugs.dm b/code/modules/reagents/chemistry/recipes/drugs.dm
index 5b46a68cb5fac2..2d58aed05cfcc6 100644
--- a/code/modules/reagents/chemistry/recipes/drugs.dm
+++ b/code/modules/reagents/chemistry/recipes/drugs.dm
@@ -14,10 +14,71 @@
mix_message = "The mixture dries into a pale blue powder."
required_temp = 380
+
/datum/chemical_reaction/methamphetamine
results = list(/datum/reagent/drug/methamphetamine = 4)
required_reagents = list(/datum/reagent/medicine/ephedrine = 1, /datum/reagent/iodine = 1, /datum/reagent/phosphorus = 1, /datum/reagent/hydrogen = 1)
- required_temp = 374
+ required_temp = 372
+ optimal_temp = 376//Wow this is tight
+ overheat_temp = 380
+ optimal_ph_min = 6.5
+ optimal_ph_max = 7.5
+ determin_ph_range = 5
+ temp_exponent_factor = 1
+ ph_exponent_factor = 1.4
+ thermic_constant = 0.1 //exothermic nature is equal to impurty
+ H_ion_release = -0.025
+ rate_up_lim = 12.5
+ purity_min = 0.5 //100u will natrually just dip under this w/ no buffer
+ reaction_flags = REACTION_HEAT_ARBITARY //Heating up is arbitary because of submechanics of this reaction.
+
+//The less pure it is, the faster it heats up. tg please don't hate me for making your meth even more dangerous
+/datum/chemical_reaction/methamphetamine/reaction_step(datum/equilibrium/reaction, datum/reagents/holder, delta_t, delta_ph, step_reaction_vol)
+ var/datum/reagent/meth = holder.get_reagent(/datum/reagent/drug/methamphetamine)
+ if(!meth)//First step
+ reaction.thermic_mod = (1-delta_ph)*5
+ return
+ reaction.thermic_mod = (1-meth.purity)*5
+
+/datum/chemical_reaction/methamphetamine/overheated(datum/reagents/holder, datum/equilibrium/equilibrium)
+ . = ..()
+ temp_meth_explosion(holder, equilibrium.reacted_vol)
+
+/datum/chemical_reaction/methamphetamine/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium)
+ . = ..()
+ temp_meth_explosion(holder, equilibrium.reacted_vol)
+
+/datum/chemical_reaction/methamphetamine/reaction_finish(datum/reagents/holder, react_vol)
+ var/datum/reagent/meth = holder.get_reagent(/datum/reagent/drug/methamphetamine)
+ if(!meth)//Other procs before this can already blow us up
+ return ..()
+ if(meth.purity < purity_min)
+ temp_meth_explosion(holder, react_vol)
+ return
+ return ..()
+
+//Refactoring of explosions is coming later, this is till then so it still explodes
+/datum/chemical_reaction/methamphetamine/proc/temp_meth_explosion(datum/reagents/holder, explode_vol)
+ var/power = 5 + round(explode_vol/12, 1) //meth strengthdiv is 12
+ if(power <= 0)
+ return
+ var/turf/T = get_turf(holder.my_atom)
+ var/inside_msg
+ if(ismob(holder.my_atom))
+ var/mob/M = holder.my_atom
+ inside_msg = " inside [ADMIN_LOOKUPFLW(M)]"
+ var/lastkey = holder.my_atom.fingerprintslast
+ var/touch_msg = "N/A"
+ if(lastkey)
+ var/mob/toucher = get_mob_by_key(lastkey)
+ touch_msg = "[ADMIN_LOOKUPFLW(toucher)]"
+ if(!istype(holder.my_atom, /obj/machinery/plumbing)) //excludes standard plumbing equipment from spamming admins with this shit
+ message_admins("Reagent explosion reaction occurred at [ADMIN_VERBOSEJMP(T)][inside_msg]. Last Fingerprint: [touch_msg].")
+ log_game("Reagent explosion reaction occurred at [AREACOORD(T)]. Last Fingerprint: [lastkey ? lastkey : "N/A"]." )
+ var/datum/effect_system/reagents_explosion/e = new()
+ e.set_up(power, T, 0, 0)
+ e.start()
+ holder.clear_reagents()
/datum/chemical_reaction/bath_salts
results = list(/datum/reagent/drug/bath_salts = 7)
diff --git a/code/modules/reagents/chemistry/recipes/medicine.dm b/code/modules/reagents/chemistry/recipes/medicine.dm
index 0b940900c72521..b542601975f65a 100644
--- a/code/modules/reagents/chemistry/recipes/medicine.dm
+++ b/code/modules/reagents/chemistry/recipes/medicine.dm
@@ -1,181 +1,190 @@
-/datum/chemical_reaction/leporazine
+/datum/chemical_reaction/medicine
+ required_reagents = null //Don't add this to master list
+ optimal_temp = 700
+ optimal_ph_max = 10
+ temp_exponent_factor = 1.2
+ ph_exponent_factor = 0.8
+ purity_min = 0.1
+ rate_up_lim = 35
+
+/datum/chemical_reaction/medicine/leporazine
results = list(/datum/reagent/medicine/leporazine = 2)
required_reagents = list(/datum/reagent/silicon = 1, /datum/reagent/copper = 1)
required_catalysts = list(/datum/reagent/toxin/plasma = 5)
-/datum/chemical_reaction/rezadone
+/datum/chemical_reaction/medicine/rezadone
results = list(/datum/reagent/medicine/rezadone = 3)
required_reagents = list(/datum/reagent/toxin/carpotoxin = 1, /datum/reagent/cryptobiolin = 1, /datum/reagent/copper = 1)
-/datum/chemical_reaction/spaceacillin
+/datum/chemical_reaction/medicine/spaceacillin
results = list(/datum/reagent/medicine/spaceacillin = 2)
required_reagents = list(/datum/reagent/cryptobiolin = 1, /datum/reagent/medicine/epinephrine = 1)
-/datum/chemical_reaction/oculine
+/datum/chemical_reaction/medicine/oculine
results = list(/datum/reagent/medicine/oculine = 3)
required_reagents = list(/datum/reagent/medicine/c2/multiver = 1, /datum/reagent/carbon = 1, /datum/reagent/hydrogen = 1)
mix_message = "The mixture bubbles noticeably and becomes a dark grey color!"
-/datum/chemical_reaction/inacusiate
+/datum/chemical_reaction/medicine/inacusiate
results = list(/datum/reagent/medicine/inacusiate = 2)
required_reagents = list(/datum/reagent/water = 1, /datum/reagent/carbon = 1, /datum/reagent/medicine/c2/multiver = 1)
mix_message = "The mixture sputters loudly and becomes a light grey color!"
-/datum/chemical_reaction/synaptizine
+/datum/chemical_reaction/medicine/synaptizine
results = list(/datum/reagent/medicine/synaptizine = 3)
required_reagents = list(/datum/reagent/consumable/sugar = 1, /datum/reagent/lithium = 1, /datum/reagent/water = 1)
-/datum/chemical_reaction/salglu_solution
+/datum/chemical_reaction/medicine/salglu_solution
results = list(/datum/reagent/medicine/salglu_solution = 3)
required_reagents = list(/datum/reagent/consumable/salt = 1, /datum/reagent/water = 1, /datum/reagent/consumable/sugar = 1)
-/datum/chemical_reaction/mine_salve
+/datum/chemical_reaction/medicine/mine_salve
results = list(/datum/reagent/medicine/mine_salve = 3)
required_reagents = list(/datum/reagent/fuel/oil = 1, /datum/reagent/water = 1, /datum/reagent/iron = 1)
-/datum/chemical_reaction/mine_salve2
+/datum/chemical_reaction/medicine/mine_salve2
results = list(/datum/reagent/medicine/mine_salve = 15)
required_reagents = list(/datum/reagent/toxin/plasma = 5, /datum/reagent/iron = 5, /datum/reagent/consumable/sugar = 1) // A sheet of plasma, a twinkie and a sheet of metal makes four of these
-/datum/chemical_reaction/synthflesh
+/datum/chemical_reaction/medicine/synthflesh
results = list(/datum/reagent/medicine/c2/synthflesh = 3)
required_reagents = list(/datum/reagent/blood = 1, /datum/reagent/carbon = 1, /datum/reagent/medicine/c2/libital = 1)
-/datum/chemical_reaction/calomel
+/datum/chemical_reaction/medicine/calomel
results = list(/datum/reagent/medicine/calomel = 2)
required_reagents = list(/datum/reagent/mercury = 1, /datum/reagent/chlorine = 1)
required_temp = 374
-/datum/chemical_reaction/potass_iodide
+/datum/chemical_reaction/medicine/potass_iodide
results = list(/datum/reagent/medicine/potass_iodide = 2)
required_reagents = list(/datum/reagent/potassium = 1, /datum/reagent/iodine = 1)
-/datum/chemical_reaction/pen_acid
+/datum/chemical_reaction/medicine/pen_acid
results = list(/datum/reagent/medicine/pen_acid = 6)
required_reagents = list(/datum/reagent/fuel = 1, /datum/reagent/chlorine = 1, /datum/reagent/ammonia = 1, /datum/reagent/toxin/formaldehyde = 1, /datum/reagent/sodium = 1, /datum/reagent/toxin/cyanide = 1)
-/datum/chemical_reaction/sal_acid
+/datum/chemical_reaction/medicine/sal_acid
results = list(/datum/reagent/medicine/sal_acid = 5)
required_reagents = list(/datum/reagent/sodium = 1, /datum/reagent/phenol = 1, /datum/reagent/carbon = 1, /datum/reagent/oxygen = 1, /datum/reagent/toxin/acid = 1)
-/datum/chemical_reaction/oxandrolone
+/datum/chemical_reaction/medicine/oxandrolone
results = list(/datum/reagent/medicine/oxandrolone = 6)
required_reagents = list(/datum/reagent/carbon = 3, /datum/reagent/phenol = 1, /datum/reagent/hydrogen = 1, /datum/reagent/oxygen = 1)
-/datum/chemical_reaction/salbutamol
+/datum/chemical_reaction/medicine/salbutamol
results = list(/datum/reagent/medicine/salbutamol = 5)
required_reagents = list(/datum/reagent/medicine/sal_acid = 1, /datum/reagent/lithium = 1, /datum/reagent/aluminium = 1, /datum/reagent/bromine = 1, /datum/reagent/ammonia = 1)
-/datum/chemical_reaction/ephedrine
+/datum/chemical_reaction/medicine/ephedrine
results = list(/datum/reagent/medicine/ephedrine = 4)
required_reagents = list(/datum/reagent/consumable/sugar = 1, /datum/reagent/fuel/oil = 1, /datum/reagent/hydrogen = 1, /datum/reagent/diethylamine = 1)
mix_message = "The solution fizzes and gives off toxic fumes."
-/datum/chemical_reaction/diphenhydramine
+/datum/chemical_reaction/medicine/diphenhydramine
results = list(/datum/reagent/medicine/diphenhydramine = 4)
required_reagents = list(/datum/reagent/fuel/oil = 1, /datum/reagent/carbon = 1, /datum/reagent/bromine = 1, /datum/reagent/diethylamine = 1, /datum/reagent/consumable/ethanol = 1)
mix_message = "The mixture dries into a pale blue powder."
-/datum/chemical_reaction/atropine
+/datum/chemical_reaction/medicine/atropine
results = list(/datum/reagent/medicine/atropine = 5)
required_reagents = list(/datum/reagent/consumable/ethanol = 1, /datum/reagent/acetone = 1, /datum/reagent/diethylamine = 1, /datum/reagent/phenol = 1, /datum/reagent/toxin/acid = 1)
-/datum/chemical_reaction/epinephrine
+/datum/chemical_reaction/medicine/epinephrine
results = list(/datum/reagent/medicine/epinephrine = 6)
required_reagents = list(/datum/reagent/phenol = 1, /datum/reagent/acetone = 1, /datum/reagent/diethylamine = 1, /datum/reagent/oxygen = 1, /datum/reagent/chlorine = 1, /datum/reagent/hydrogen = 1)
-/datum/chemical_reaction/strange_reagent
+/datum/chemical_reaction/medicine/strange_reagent
results = list(/datum/reagent/medicine/strange_reagent = 3)
required_reagents = list(/datum/reagent/medicine/omnizine = 1, /datum/reagent/water/holywater = 1, /datum/reagent/toxin/mutagen = 1)
-/datum/chemical_reaction/strange_reagent/alt
+/datum/chemical_reaction/medicine/strange_reagent/alt
results = list(/datum/reagent/medicine/strange_reagent = 2)
required_reagents = list(/datum/reagent/medicine/omnizine/protozine = 1, /datum/reagent/water/holywater = 1, /datum/reagent/toxin/mutagen = 1)
-/datum/chemical_reaction/mannitol
+/datum/chemical_reaction/medicine/mannitol
results = list(/datum/reagent/medicine/mannitol = 3)
required_reagents = list(/datum/reagent/consumable/sugar = 1, /datum/reagent/hydrogen = 1, /datum/reagent/water = 1)
mix_message = "The solution slightly bubbles, becoming thicker."
-/datum/chemical_reaction/neurine
+/datum/chemical_reaction/medicine/neurine
results = list(/datum/reagent/medicine/neurine = 3)
required_reagents = list(/datum/reagent/medicine/mannitol = 1, /datum/reagent/acetone = 1, /datum/reagent/oxygen = 1)
-/datum/chemical_reaction/mutadone
+/datum/chemical_reaction/medicine/mutadone
results = list(/datum/reagent/medicine/mutadone = 3)
required_reagents = list(/datum/reagent/toxin/mutagen = 1, /datum/reagent/acetone = 1, /datum/reagent/bromine = 1)
-/datum/chemical_reaction/antihol
+/datum/chemical_reaction/medicine/antihol
results = list(/datum/reagent/medicine/antihol = 3)
required_reagents = list(/datum/reagent/consumable/ethanol = 1, /datum/reagent/medicine/c2/multiver = 1, /datum/reagent/copper = 1)
-/datum/chemical_reaction/cryoxadone
+/datum/chemical_reaction/medicine/cryoxadone
results = list(/datum/reagent/medicine/cryoxadone = 3)
required_reagents = list(/datum/reagent/stable_plasma = 1, /datum/reagent/acetone = 1, /datum/reagent/toxin/mutagen = 1)
-/datum/chemical_reaction/pyroxadone
+/datum/chemical_reaction/medicine/pyroxadone
results = list(/datum/reagent/medicine/pyroxadone = 2)
required_reagents = list(/datum/reagent/medicine/cryoxadone = 1, /datum/reagent/toxin/slimejelly = 1)
-/datum/chemical_reaction/clonexadone
+/datum/chemical_reaction/medicine/clonexadone
results = list(/datum/reagent/medicine/clonexadone = 2)
required_reagents = list(/datum/reagent/medicine/cryoxadone = 1, /datum/reagent/sodium = 1)
required_catalysts = list(/datum/reagent/toxin/plasma = 5)
-/datum/chemical_reaction/haloperidol
+/datum/chemical_reaction/medicine/haloperidol
results = list(/datum/reagent/medicine/haloperidol = 5)
required_reagents = list(/datum/reagent/chlorine = 1, /datum/reagent/fluorine = 1, /datum/reagent/aluminium = 1, /datum/reagent/medicine/potass_iodide = 1, /datum/reagent/fuel/oil = 1)
-/datum/chemical_reaction/regen_jelly
+/datum/chemical_reaction/medicine/regen_jelly
results = list(/datum/reagent/medicine/regen_jelly = 2)
required_reagents = list(/datum/reagent/medicine/omnizine = 1, /datum/reagent/toxin/slimejelly = 1)
-/datum/chemical_reaction/higadrite
+/datum/chemical_reaction/medicine/higadrite
results = list(/datum/reagent/medicine/higadrite = 3)
required_reagents = list(/datum/reagent/phenol = 2, /datum/reagent/lithium = 1)
-/datum/chemical_reaction/morphine
+/datum/chemical_reaction/medicine/morphine
results = list(/datum/reagent/medicine/morphine = 2)
required_reagents = list(/datum/reagent/carbon = 2, /datum/reagent/hydrogen = 2, /datum/reagent/consumable/ethanol = 1, /datum/reagent/oxygen = 1)
required_temp = 480
-/datum/chemical_reaction/modafinil
+/datum/chemical_reaction/medicine/modafinil
results = list(/datum/reagent/medicine/modafinil = 5)
required_reagents = list(/datum/reagent/diethylamine = 1, /datum/reagent/ammonia = 1, /datum/reagent/phenol = 1, /datum/reagent/acetone = 1, /datum/reagent/toxin/acid = 1)
required_catalysts = list(/datum/reagent/bromine = 1) // as close to the real world synthesis as possible
-/datum/chemical_reaction/psicodine
+/datum/chemical_reaction/medicine/psicodine
results = list(/datum/reagent/medicine/psicodine = 5)
required_reagents = list( /datum/reagent/medicine/mannitol = 2, /datum/reagent/water = 2, /datum/reagent/impedrezene = 1)
-/datum/chemical_reaction/granibitaluri
+/datum/chemical_reaction/medicine/granibitaluri
results = list(/datum/reagent/medicine/granibitaluri = 3)
required_reagents = list(/datum/reagent/consumable/salt = 1, /datum/reagent/carbon = 1, /datum/reagent/toxin/acid = 1)
required_catalysts = list(/datum/reagent/iron = 5)
///medical stacks
-/datum/chemical_reaction/medsuture
+/datum/chemical_reaction/medicine/medsuture
required_reagents = list(/datum/reagent/cellulose = 10, /datum/reagent/toxin/formaldehyde = 20, /datum/reagent/medicine/polypyr = 15) //This might be a bit much, reagent cost should be reviewed after implementation.
-/datum/chemical_reaction/medsuture/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/medicine/medsuture/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/stack/medical/suture/medicated(location)
-/datum/chemical_reaction/medmesh
+/datum/chemical_reaction/medicine/medmesh
required_reagents = list(/datum/reagent/cellulose = 20, /datum/reagent/consumable/aloejuice = 20, /datum/reagent/space_cleaner/sterilizine = 10)
-/datum/chemical_reaction/medmesh/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/medicine/medmesh/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/stack/medical/mesh/advanced(location)
-/datum/chemical_reaction/poultice
+/datum/chemical_reaction/medicine/poultice
required_reagents = list(/datum/reagent/toxin/bungotoxin = 20, /datum/reagent/cellulose = 20, /datum/reagent/consumable/aloejuice = 20)
-/datum/chemical_reaction/poultice/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/medicine/poultice/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i in 1 to created_volume)
new /obj/item/stack/medical/poultice(location)
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index 978e23daa6d8df..e9ffa8a4f4be4d 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -39,8 +39,9 @@
/datum/chemical_reaction/plasma_solidification
required_reagents = list(/datum/reagent/iron = 5, /datum/reagent/consumable/frostoil = 5, /datum/reagent/toxin/plasma = 20)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/plasma_solidification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/plasma_solidification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/stack/sheet/mineral/plasma(location)
@@ -48,8 +49,9 @@
/datum/chemical_reaction/gold_solidification
required_reagents = list(/datum/reagent/consumable/frostoil = 5, /datum/reagent/gold = 20, /datum/reagent/iron = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/gold_solidification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/gold_solidification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/stack/sheet/mineral/gold(location)
@@ -57,8 +59,9 @@
/datum/chemical_reaction/uranium_solidification
required_reagents = list(/datum/reagent/consumable/frostoil = 5, /datum/reagent/uranium = 20, /datum/reagent/potassium = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/uranium_solidification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/uranium_solidification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/stack/sheet/mineral/uranium(location)
@@ -71,8 +74,9 @@
required_reagents = list(/datum/reagent/liquidgibs = 10, /datum/reagent/lye = 10) // requires two scooped gib tiles
required_temp = 374
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/soapification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/soapification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/soap/homemade(location)
@@ -81,8 +85,9 @@
required_reagents = list(/datum/reagent/consumable/potato_juice = 10, /datum/reagent/consumable/ethanol/lizardwine = 10, /datum/reagent/monkey_powder = 10, /datum/reagent/drug/krokodil = 10, /datum/reagent/toxin/acid/nitracid = 10, /datum/reagent/baldium = 10, /datum/reagent/consumable/ethanol/hooch = 10, /datum/reagent/bluespace = 10, /datum/reagent/drug/pumpup = 10, /datum/reagent/consumable/space_cola = 10)
required_temp = 999
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/omegasoapification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/omegasoapification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/soap/omega(location)
@@ -91,8 +96,9 @@
required_reagents = list(/datum/reagent/liquidgibs = 5, /datum/reagent/oxygen = 5) //
required_temp = 374
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/candlefication/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/candlefication/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/candle(location)
@@ -100,8 +106,9 @@
/datum/chemical_reaction/meatification
required_reagents = list(/datum/reagent/liquidgibs = 10, /datum/reagent/consumable/nutriment = 10, /datum/reagent/carbon = 10)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/meatification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/meatification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = 1, i <= created_volume, i++)
new /obj/item/food/meat/slab/meatproduct(location)
@@ -175,8 +182,9 @@
required_catalysts = list(/datum/reagent/blood = 1)
var/level_min = 1
var/level_max = 2
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/mix_virus/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/mix_virus/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/datum/reagent/blood/B = locate(/datum/reagent/blood) in holder.reagent_list
if(B?.data)
var/datum/disease/advance/D = locate(/datum/disease/advance) in B.data["viruses"]
@@ -243,7 +251,7 @@
required_reagents = list(/datum/reagent/medicine/synaptizine = 1)
required_catalysts = list(/datum/reagent/blood = 1)
-/datum/chemical_reaction/mix_virus/rem_virus/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/mix_virus/rem_virus/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/datum/reagent/blood/B = locate(/datum/reagent/blood) in holder.reagent_list
if(B?.data)
var/datum/disease/advance/D = locate(/datum/disease/advance) in B.data["viruses"]
@@ -254,7 +262,7 @@
required_reagents = list(/datum/reagent/toxin/formaldehyde = 1)
required_catalysts = list(/datum/reagent/blood = 1)
-/datum/chemical_reaction/mix_virus/neuter_virus/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/mix_virus/neuter_virus/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/datum/reagent/blood/B = locate(/datum/reagent/blood) in holder.reagent_list
if(B?.data)
var/datum/disease/advance/D = locate(/datum/disease/advance) in B.data["viruses"]
@@ -269,33 +277,38 @@
/datum/chemical_reaction/surfactant
results = list(/datum/reagent/fluorosurfactant = 5)
required_reagents = list(/datum/reagent/fluorine = 2, /datum/reagent/carbon = 2, /datum/reagent/toxin/acid = 1)
+ reaction_flags = REACTION_INSTANT
/datum/chemical_reaction/foam
required_reagents = list(/datum/reagent/fluorosurfactant = 1, /datum/reagent/water = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/foam/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/foam/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.create_foam(/datum/effect_system/foam_spread,2*created_volume,notification="The solution spews out foam!")
/datum/chemical_reaction/metalfoam
required_reagents = list(/datum/reagent/aluminium = 3, /datum/reagent/foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/metalfoam/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/metalfoam/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.create_foam(/datum/effect_system/foam_spread/metal,5*created_volume,1,"The solution spews out a metallic foam!")
/datum/chemical_reaction/smart_foam
required_reagents = list(/datum/reagent/aluminium = 3, /datum/reagent/smart_foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
mob_react = TRUE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/smart_foam/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/smart_foam/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.create_foam(/datum/effect_system/foam_spread/metal/smart,5*created_volume,1,"The solution spews out metallic foam!")
/datum/chemical_reaction/ironfoam
required_reagents = list(/datum/reagent/iron = 3, /datum/reagent/foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/ironfoam/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/ironfoam/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.create_foam(/datum/effect_system/foam_spread/metal,5*created_volume,2,"The solution spews out a metallic foam!")
/datum/chemical_reaction/foaming_agent
@@ -313,6 +326,9 @@
/datum/chemical_reaction/ammonia
results = list(/datum/reagent/ammonia = 3)
required_reagents = list(/datum/reagent/hydrogen = 3, /datum/reagent/nitrogen = 1)
+ optimal_ph_min = 1 // Lets increase our range for this basic chem
+ optimal_ph_max = 12
+ H_ion_release = -0.02 //handmade is more neutral
/datum/chemical_reaction/diethylamine
results = list(/datum/reagent/diethylamine = 2)
@@ -321,6 +337,7 @@
/datum/chemical_reaction/space_cleaner
results = list(/datum/reagent/space_cleaner = 2)
required_reagents = list(/datum/reagent/ammonia = 1, /datum/reagent/water = 1)
+ rate_up_lim = 40
/datum/chemical_reaction/plantbgone
results = list(/datum/reagent/toxin/plantbgone = 5)
@@ -329,6 +346,7 @@
/datum/chemical_reaction/weedkiller
results = list(/datum/reagent/toxin/plantbgone/weedkiller = 5)
required_reagents = list(/datum/reagent/toxin = 1, /datum/reagent/ammonia = 4)
+ H_ion_release = -0.05 // Push towards acidic
/datum/chemical_reaction/pestkiller
results = list(/datum/reagent/toxin/pestkiller = 5)
@@ -408,22 +426,25 @@
/datum/chemical_reaction/life
required_reagents = list(/datum/reagent/medicine/strange_reagent = 1, /datum/reagent/medicine/c2/synthflesh = 1, /datum/reagent/blood = 1)
required_temp = 374
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/life/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/life/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
chemical_mob_spawn(holder, rand(1, round(created_volume, 1)), "Life (hostile)") //defaults to HOSTILE_SPAWN
/datum/chemical_reaction/life_friendly
required_reagents = list(/datum/reagent/medicine/strange_reagent = 1, /datum/reagent/medicine/c2/synthflesh = 1, /datum/reagent/consumable/sugar = 1)
required_temp = 374
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/life_friendly/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/life_friendly/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
chemical_mob_spawn(holder, rand(1, round(created_volume, 1)), "Life (friendly)", FRIENDLY_SPAWN)
/datum/chemical_reaction/corgium
required_reagents = list(/datum/reagent/consumable/nutriment = 1, /datum/reagent/colorful_reagent = 1, /datum/reagent/medicine/strange_reagent = 1, /datum/reagent/blood = 1)
required_temp = 374
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/corgium/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/corgium/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = rand(1, created_volume), i <= created_volume, i++) // More lulz.
new /mob/living/simple_animal/pet/dog/corgi(location)
@@ -433,12 +454,13 @@
/datum/chemical_reaction/monkey_powder
results = list(/datum/reagent/monkey_powder = 5)
required_reagents = list(/datum/reagent/consumable/banana = 1, /datum/reagent/consumable/nutriment=2,/datum/reagent/liquidgibs = 1)
+ reaction_flags = REACTION_INSTANT
/datum/chemical_reaction/monkey
required_reagents = list(/datum/reagent/monkey_powder = 50, /datum/reagent/water = 1)
mix_message = "Expands into a brown mass before shaping itself into a monkey!."
-/datum/chemical_reaction/monkey/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/monkey/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/mob/living/carbon/M = holder.my_atom
var/location = get_turf(M)
if(istype(M, /mob/living/carbon))
@@ -456,8 +478,9 @@
//butterflium
/datum/chemical_reaction/butterflium
required_reagents = list(/datum/reagent/colorful_reagent = 1, /datum/reagent/medicine/omnizine = 1, /datum/reagent/medicine/strange_reagent = 1, /datum/reagent/consumable/nutriment = 1)
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/butterflium/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/butterflium/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i = rand(1, created_volume), i <= created_volume, i++)
new /mob/living/simple_animal/butterfly(location)
@@ -466,8 +489,9 @@
/datum/chemical_reaction/scream
required_reagents = list(/datum/reagent/medicine/strange_reagent = 1, /datum/reagent/consumable/cream = 5, /datum/reagent/consumable/ethanol/lizardwine = 5 )
required_temp = 374
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/scream/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/scream/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
playsound(holder.my_atom, pick(list( 'sound/voice/human/malescream_1.ogg', 'sound/voice/human/malescream_2.ogg', 'sound/voice/human/malescream_3.ogg', 'sound/voice/human/malescream_4.ogg', 'sound/voice/human/malescream_5.ogg', 'sound/voice/human/malescream_6.ogg', 'sound/voice/human/femalescream_1.ogg', 'sound/voice/human/femalescream_2.ogg', 'sound/voice/human/femalescream_3.ogg', 'sound/voice/human/femalescream_4.ogg', 'sound/voice/human/femalescream_5.ogg', 'sound/voice/human/wilhelm_scream.ogg')), created_volume*5,TRUE)
/datum/chemical_reaction/hair_dye
@@ -494,6 +518,7 @@
/datum/chemical_reaction/lye
results = list(/datum/reagent/lye = 3)
required_reagents = list(/datum/reagent/sodium = 1, /datum/reagent/hydrogen = 1, /datum/reagent/oxygen = 1)
+ required_temp = 10 //So hercuri still shows life.
/datum/chemical_reaction/lye2
results = list(/datum/reagent/lye = 2)
@@ -510,8 +535,9 @@
/datum/chemical_reaction/plastic_polymers
required_reagents = list(/datum/reagent/fuel/oil = 5, /datum/reagent/toxin/acid = 2, /datum/reagent/ash = 3)
required_temp = 374 //lazily consistent with soap & other crafted objects generically created with heat.
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/plastic_polymers/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/plastic_polymers/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i in 1 to created_volume)
new /obj/item/stack/sheet/plastic(location)
@@ -535,16 +561,18 @@
/datum/chemical_reaction/slime_extractification
required_reagents = list(/datum/reagent/toxin/slimejelly = 30, /datum/reagent/consumable/frostoil = 5, /datum/reagent/toxin/plasma = 5)
mix_message = "The mixture condenses into a ball."
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/slime_extractification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime_extractification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
new /obj/item/slime_extract/grey(location)
/datum/chemical_reaction/metalgen_imprint
required_reagents = list(/datum/reagent/metalgen = 1, /datum/reagent/liquid_dark_matter = 1)
results = list(/datum/reagent/metalgen = 1)
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/metalgen_imprint/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/metalgen_imprint/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/datum/reagent/metalgen/MM = holder.get_reagent(/datum/reagent/metalgen)
for(var/datum/reagent/R in holder.reagent_list)
if(R.material && R.volume >= 40)
@@ -590,8 +618,9 @@
required_reagents = list(/datum/reagent/silver = 20, /datum/reagent/carbon = 10)
required_temp = 630
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/silver_solidification/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/silver_solidification/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
for(var/i in 1 to created_volume)
new /obj/item/stack/sheet/mineral/silver(location)
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index 4e36b2e83148d5..a4ea6b5a92695c 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -1,8 +1,9 @@
/datum/chemical_reaction/reagent_explosion
var/strengthdiv = 10
var/modifier = 0
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/reagent_explosion/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
explode(holder, created_volume)
/datum/chemical_reaction/reagent_explosion/proc/explode(datum/reagents/holder, created_volume)
@@ -13,7 +14,7 @@
if(ismob(holder.my_atom))
var/mob/M = holder.my_atom
inside_msg = " inside [ADMIN_LOOKUPFLW(M)]"
- var/lastkey = holder.my_atom.fingerprintslast
+ var/lastkey = holder.my_atom.fingerprintslast //This can runtime (null.fingerprintslast) - due to plumbing?
var/touch_msg = "N/A"
if(lastkey)
var/mob/toucher = get_mob_by_key(lastkey)
@@ -31,7 +32,7 @@
required_reagents = list(/datum/reagent/glycerol = 1, /datum/reagent/toxin/acid/nitracid = 1, /datum/reagent/toxin/acid = 1)
strengthdiv = 2
-/datum/chemical_reaction/reagent_explosion/nitroglycerin/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/nitroglycerin/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/exotic_stabilizer,round(created_volume / 25, CHEMICAL_QUANTISATION_LEVEL)))
return
@@ -50,7 +51,7 @@
required_temp = 404
strengthdiv = 8
-/datum/chemical_reaction/reagent_explosion/rdx/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/rdx/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/rdx, created_volume*2)
@@ -67,7 +68,7 @@
strengthdiv = 3.5 //actually a decrease of 1 becaused of how explosions are calculated. This is due to the fact we require 2 reagents
modifier = 4
-/datum/chemical_reaction/reagent_explosion/rdx_explosion2/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/rdx_explosion2/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/fire_range = round(created_volume/30)
var/turf/T = get_turf(holder.my_atom)
for(var/turf/turf in range(fire_range,T))
@@ -81,7 +82,7 @@
modifier = 6
-/datum/chemical_reaction/reagent_explosion/rdx_explosion3/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/rdx_explosion3/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/fire_range = round(created_volume/20)
var/turf/T = get_turf(holder.my_atom)
for(var/turf/turf in range(fire_range,T))
@@ -98,7 +99,7 @@
/datum/chemical_reaction/reagent_explosion/tatp/update_info()
required_temp = 450 + rand(-49,49) //this gets loaded only on round start
-/datum/chemical_reaction/reagent_explosion/tatp/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/tatp/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/exotic_stabilizer,round(created_volume / 50, CHEMICAL_QUANTISATION_LEVEL))) // we like exotic stabilizer
return
holder.remove_reagent(/datum/reagent/tatp, created_volume)
@@ -109,7 +110,7 @@
required_temp = 550 // this makes making tatp before pyro nades, and extreme pain in the ass to make
strengthdiv = 3
-/datum/chemical_reaction/reagent_explosion/tatp_explosion/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/tatp_explosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/strengthdiv_adjust = created_volume / ( 2100 / initial(strengthdiv))
strengthdiv = max(initial(strengthdiv) - strengthdiv_adjust + 1.5 ,1.5) //Slightly better than nitroglycerin
. = ..()
@@ -135,7 +136,7 @@
required_reagents = list(/datum/reagent/water/holywater = 1, /datum/reagent/potassium = 1)
strengthdiv = 20
-/datum/chemical_reaction/reagent_explosion/holyboom/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/holyboom/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(created_volume >= 150)
strengthdiv = 8
///turf where to play sound
@@ -172,7 +173,7 @@
modifier = 5
mix_message = "Sparks start flying around the gunpowder!"
-/datum/chemical_reaction/reagent_explosion/gunpowder_explosion/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/gunpowder_explosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
addtimer(CALLBACK(src, .proc/explode, holder, created_volume), rand(5,10) SECONDS)
/datum/chemical_reaction/thermite
@@ -182,7 +183,7 @@
/datum/chemical_reaction/emp_pulse
required_reagents = list(/datum/reagent/uranium = 1, /datum/reagent/iron = 1) // Yes, laugh, it's the best recipe I could think of that makes a little bit of sense
-/datum/chemical_reaction/emp_pulse/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/emp_pulse/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
// 100 created volume = 4 heavy range & 7 light range. A few tiles smaller than traitor EMP grandes.
// 200 created volume = 8 heavy range & 14 light range. 4 tiles larger than traitor EMP grenades.
@@ -193,7 +194,7 @@
/datum/chemical_reaction/beesplosion
required_reagents = list(/datum/reagent/consumable/honey = 1, /datum/reagent/medicine/strange_reagent = 1, /datum/reagent/uranium/radium = 1)
-/datum/chemical_reaction/beesplosion/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/beesplosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = holder.my_atom.drop_location()
if(created_volume < 5)
playsound(location,'sound/effects/sparks1.ogg', 100, TRUE)
@@ -219,8 +220,9 @@
results = list(/datum/reagent/clf3 = 4)
required_reagents = list(/datum/reagent/chlorine = 1, /datum/reagent/fluorine = 3)
required_temp = 424
+ overheat_temp = 1050
-/datum/chemical_reaction/clf3/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/clf3/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
for(var/turf/turf in range(1,T))
new /obj/effect/hotspot(turf)
@@ -233,7 +235,7 @@
modifier = 5
mob_react = FALSE
-/datum/chemical_reaction/reagent_explosion/methsplosion/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/methsplosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
for(var/turf/turf in range(1,T))
new /obj/effect/hotspot(turf)
@@ -248,7 +250,7 @@
results = list(/datum/reagent/sorium = 4)
required_reagents = list(/datum/reagent/mercury = 1, /datum/reagent/oxygen = 1, /datum/reagent/nitrogen = 1, /datum/reagent/carbon = 1)
-/datum/chemical_reaction/sorium/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/sorium/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/sorium, created_volume*4)
@@ -260,7 +262,7 @@
required_reagents = list(/datum/reagent/sorium = 1)
required_temp = 474
-/datum/chemical_reaction/sorium_vortex/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/sorium_vortex/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
var/range = clamp(sqrt(created_volume), 1, 6)
goonchem_vortex(T, 1, range)
@@ -269,7 +271,7 @@
results = list(/datum/reagent/liquid_dark_matter = 3)
required_reagents = list(/datum/reagent/stable_plasma = 1, /datum/reagent/uranium/radium = 1, /datum/reagent/carbon = 1)
-/datum/chemical_reaction/liquid_dark_matter/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/liquid_dark_matter/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/liquid_dark_matter, created_volume*3)
@@ -281,7 +283,7 @@
required_reagents = list(/datum/reagent/liquid_dark_matter = 1)
required_temp = 474
-/datum/chemical_reaction/ldm_vortex/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/ldm_vortex/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
var/range = clamp(sqrt(created_volume/2), 1, 6)
goonchem_vortex(T, 0, range)
@@ -289,8 +291,9 @@
/datum/chemical_reaction/flash_powder
results = list(/datum/reagent/flash_powder = 3)
required_reagents = list(/datum/reagent/aluminium = 1, /datum/reagent/potassium = 1, /datum/reagent/sulfur = 1 )
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/flash_powder/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/flash_powder/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
var/location = get_turf(holder.my_atom)
@@ -311,7 +314,7 @@
required_reagents = list(/datum/reagent/flash_powder = 1)
required_temp = 374
-/datum/chemical_reaction/flash_powder_flash/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/flash_powder_flash/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
do_sparks(2, TRUE, location)
var/range = created_volume/10
@@ -328,8 +331,9 @@
/datum/chemical_reaction/smoke_powder
results = list(/datum/reagent/smoke_powder = 3)
required_reagents = list(/datum/reagent/potassium = 1, /datum/reagent/consumable/sugar = 1, /datum/reagent/phosphorus = 1)
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/smoke_powder/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/smoke_powder/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/smoke_powder, created_volume*3)
@@ -348,8 +352,9 @@
required_reagents = list(/datum/reagent/smoke_powder = 1)
required_temp = 374
mob_react = FALSE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/smoke_powder_smoke/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/smoke_powder_smoke/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
var/smoke_radius = round(sqrt(created_volume / 2), 1)
var/datum/effect_system/smoke_spread/chem/S = new
@@ -365,7 +370,7 @@
results = list(/datum/reagent/sonic_powder = 3)
required_reagents = list(/datum/reagent/oxygen = 1, /datum/reagent/consumable/space_cola = 1, /datum/reagent/phosphorus = 1)
-/datum/chemical_reaction/sonic_powder/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/sonic_powder/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
holder.remove_reagent(/datum/reagent/sonic_powder, created_volume*3)
@@ -378,7 +383,7 @@
required_reagents = list(/datum/reagent/sonic_powder = 1)
required_temp = 374
-/datum/chemical_reaction/sonic_powder_deafen/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/sonic_powder_deafen/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/location = get_turf(holder.my_atom)
playsound(location, 'sound/effects/bang.ogg', 25, TRUE)
for(var/mob/living/carbon/C in get_hearers_in_view(created_volume/10, location))
@@ -388,7 +393,7 @@
results = list(/datum/reagent/phlogiston = 3)
required_reagents = list(/datum/reagent/phosphorus = 1, /datum/reagent/toxin/acid = 1, /datum/reagent/stable_plasma = 1)
-/datum/chemical_reaction/phlogiston/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/phlogiston/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(holder.has_reagent(/datum/reagent/stabilizing_agent))
return
var/turf/open/T = get_turf(holder.my_atom)
@@ -404,8 +409,13 @@
/datum/chemical_reaction/cryostylane
results = list(/datum/reagent/cryostylane = 3)
required_reagents = list(/datum/reagent/water = 1, /datum/reagent/stable_plasma = 1, /datum/reagent/nitrogen = 1)
+ is_cold_recipe = TRUE //This is kind of a strange reaction that I will come back to tweak later
+ required_temp = 1000
+ optimal_temp = 20
+ overheat_temp = 1
+ thermic_constant = -250
-/datum/chemical_reaction/cryostylane/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/cryostylane/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.chem_temp = 20 // cools the fuck down
return
@@ -413,8 +423,9 @@
results = list(/datum/reagent/cryostylane = 1)
required_reagents = list(/datum/reagent/cryostylane = 1, /datum/reagent/oxygen = 1)
mob_react = FALSE
+ thermic_constant = -1
-/datum/chemical_reaction/cryostylane_oxygen/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/cryostylane_oxygen/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.chem_temp = max(holder.chem_temp - 10*created_volume,0)
/datum/chemical_reaction/pyrosium_oxygen
@@ -422,14 +433,14 @@
required_reagents = list(/datum/reagent/pyrosium = 1, /datum/reagent/oxygen = 1)
mob_react = FALSE
-/datum/chemical_reaction/pyrosium_oxygen/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/pyrosium_oxygen/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.chem_temp += 10*created_volume
/datum/chemical_reaction/pyrosium
results = list(/datum/reagent/pyrosium = 3)
required_reagents = list(/datum/reagent/stable_plasma = 1, /datum/reagent/uranium/radium = 1, /datum/reagent/phosphorus = 1)
-/datum/chemical_reaction/pyrosium/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/pyrosium/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.chem_temp = 20 // also cools the fuck down
return
@@ -452,7 +463,7 @@
mix_sound = 'sound/machines/defib_zap.ogg'
var/zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_MOB_STUN
-/datum/chemical_reaction/reagent_explosion/teslium_lightning/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/teslium_lightning/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/T1 = created_volume * 20 //100 units : Zap 3 times, with powers 2000/5000/12000. Tesla revolvers have a power of 10000 for comparison.
var/T2 = created_volume * 50
var/T3 = created_volume * 120
@@ -483,7 +494,7 @@
required_temp = 575
modifier = 1
-/datum/chemical_reaction/reagent_explosion/nitrous_oxide/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/reagent_explosion/nitrous_oxide/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.remove_reagent(/datum/reagent/sorium, created_volume*2)
var/turf/turfie = get_turf(holder.my_atom)
//generally half as strong as sorium.
@@ -499,3 +510,7 @@
required_reagents = list(/datum/reagent/stabilizing_agent = 1,/datum/reagent/fluorosurfactant = 1,/datum/reagent/carbon = 1)
required_temp = 200
is_cold_recipe = 1
+ optimal_temp = 50
+ overheat_temp = 5
+ thermic_constant= -1
+ H_ion_release = -0.02
diff --git a/code/modules/reagents/chemistry/recipes/reaction_agents.dm b/code/modules/reagents/chemistry/recipes/reaction_agents.dm
new file mode 100644
index 00000000000000..4af750fbb61854
--- /dev/null
+++ b/code/modules/reagents/chemistry/recipes/reaction_agents.dm
@@ -0,0 +1,115 @@
+/datum/chemical_reaction/basic_buffer
+ results = list(/datum/reagent/reaction_agent/basic_buffer = 10)
+ required_reagents = list(/datum/reagent/ammonia = 3, /datum/reagent/chlorine = 2, /datum/reagent/hydrogen = 2, /datum/reagent/oxygen = 2) //vagely NH4OH + NH4Cl buffer
+ mix_message = "The solution fizzes in the beaker."
+ //FermiChem vars:
+ required_temp = 250
+ optimal_temp = 500
+ overheat_temp = 9999
+ optimal_ph_min = 0
+ optimal_ph_max = 14
+ determin_ph_range = 0
+ temp_exponent_factor = 4
+ ph_exponent_factor = 0
+ thermic_constant = 0
+ H_ion_release = 0.01
+ rate_up_lim = 15
+ purity_min = 0
+
+/datum/chemical_reaction/acidic_buffer
+ results = list(/datum/reagent/reaction_agent/acidic_buffer = 10)
+ required_reagents = list(/datum/reagent/sodium = 2, /datum/reagent/hydrogen = 2, /datum/reagent/consumable/ethanol = 2, /datum/reagent/water = 2)
+ mix_message = "The solution froths in the beaker."
+ required_temp = 250
+ optimal_temp = 500
+ overheat_temp = 9999
+ optimal_ph_min = 0
+ optimal_ph_max = 14
+ determin_ph_range = 0
+ temp_exponent_factor = 4
+ ph_exponent_factor = 0
+ thermic_constant = 0
+ H_ion_release = -0.01
+ rate_up_lim = 20
+ purity_min = 0
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//////////////////// Example competitive reaction (REACTION_COMPETITIVE) //////////////////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+/datum/chemical_reaction/prefactor_a
+ results = list(/datum/reagent/prefactor_a = 5)
+ required_reagents = list(/datum/reagent/phenol = 1, /datum/reagent/consumable/ethanol = 3, /datum/reagent/toxin/plasma = 1)
+ mix_message = "The solution's viscosity increases."
+ is_cold_recipe = TRUE
+ required_temp = 800
+ optimal_temp = 300
+ overheat_temp = -1 //no overheat
+ optimal_ph_min = 2
+ optimal_ph_max = 12
+ determin_ph_range = 5
+ temp_exponent_factor = 1
+ ph_exponent_factor = 0
+ thermic_constant = -400
+ H_ion_release = 0
+ rate_up_lim = 4
+ purity_min = 0.25
+
+
+/datum/chemical_reaction/prefactor_b
+ results = list(/datum/reagent/prefactor_b = 5)
+ required_reagents = list(/datum/reagent/prefactor_a = 5)
+ mix_message = "The solution's viscosity decreases."
+ mix_sound = 'sound/chemistry/bluespace.ogg' //Maybe use this elsewhere instead
+ required_temp = 50
+ optimal_temp = 500
+ overheat_temp = 500
+ optimal_ph_min = 5
+ optimal_ph_max = 8
+ determin_ph_range = 5
+ temp_exponent_factor = 1
+ ph_exponent_factor = 2
+ thermic_constant = -800
+ H_ion_release = -0.02
+ rate_up_lim = 6
+ purity_min = 0.35
+ reaction_flags = REACTION_COMPETITIVE //Competes with /datum/chemical_reaction/prefactor_a/competitive
+
+/datum/chemical_reaction/prefactor_b/reaction_step(datum/equilibrium/reaction, datum/reagents/holder, delta_t, delta_ph, step_reaction_vol)
+ . = ..()
+ if(holder.has_reagent(/datum/reagent/bluespace))
+ holder.remove_reagent(/datum/reagent/bluespace, 1)
+ reaction.delta_t *= 5
+
+/datum/chemical_reaction/prefactor_b/overheated(datum/reagents/holder, datum/equilibrium/equilibrium)
+ . = ..()
+ explode_shockwave(holder, equilibrium)
+ var/vol = max(20, holder.total_volume/5) //Not letting you have more than 5
+ clear_reagents(holder, vol)//Lest we explode forever
+
+/datum/chemical_reaction/prefactor_b/overly_impure(datum/reagents/holder, datum/equilibrium/equilibrium)
+ explode_fire(holder, equilibrium)
+ var/vol = max(20, holder.total_volume/5) //Not letting you have more than 5
+ clear_reagents(holder, vol)
+
+/datum/chemical_reaction/prefactor_a/competitive //So we have a back and forth reaction
+ results = list(/datum/reagent/prefactor_a = 5)
+ required_reagents = list(/datum/reagent/prefactor_b = 5)
+ rate_up_lim = 3
+ reaction_flags = REACTION_COMPETITIVE //Competes with /datum/chemical_reaction/prefactor_b
+
+//The actual results
+/datum/chemical_reaction/prefactor_a/purity_tester
+ results = list(/datum/reagent/reaction_agent/purity_tester = 5)
+ required_reagents = list(/datum/reagent/prefactor_a = 5, /datum/reagent/stable_plasma = 5)
+ H_ion_release = 0.05
+ thermic_constant = 0
+
+/datum/chemical_reaction/prefactor_b/speed_agent
+ results = list(/datum/reagent/reaction_agent/speed_agent = 5)
+ required_reagents = list(/datum/reagent/prefactor_b = 5, /datum/reagent/stable_plasma = 5)
+ H_ion_release = -0.15
+ thermic_constant = 0
+
+////////////////////////////////End example/////////////////////////////////////////////////////////////////////////////
diff --git a/code/modules/reagents/chemistry/recipes/slime_extracts.dm b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
index 4dc292b5375a2b..3102be75b3571d 100644
--- a/code/modules/reagents/chemistry/recipes/slime_extracts.dm
+++ b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
@@ -1,8 +1,9 @@
/datum/chemical_reaction/slime
var/deletes_extract = TRUE
+ reaction_flags = REACTION_INSTANT
-/datum/chemical_reaction/slime/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
use_slime_core(holder)
/datum/chemical_reaction/slime/proc/use_slime_core(datum/reagents/holder)
@@ -21,7 +22,7 @@
required_container = /obj/item/slime_extract/grey
required_other = TRUE
-/datum/chemical_reaction/slime/slimespawn/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimespawn/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/mob/living/simple_animal/slime/S = new(get_turf(holder.my_atom), "grey")
S.visible_message("Infused with plasma, the core begins to quiver and grow, and a new baby slime emerges from it!")
..()
@@ -37,7 +38,7 @@
required_container = /obj/item/slime_extract/grey
required_other = TRUE
-/datum/chemical_reaction/slime/slimemonkey/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimemonkey/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
for(var/i in 1 to 3)
new /obj/item/food/monkeycube(get_turf(holder.my_atom))
..()
@@ -67,7 +68,7 @@
required_container = /obj/item/slime_extract/metal
required_other = TRUE
-/datum/chemical_reaction/slime/slimemetal/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimemetal/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/location = get_turf(holder.my_atom)
new /obj/item/stack/sheet/plasteel(location, 5)
new /obj/item/stack/sheet/iron(location, 15)
@@ -78,7 +79,7 @@
required_container = /obj/item/slime_extract/metal
required_other = TRUE
-/datum/chemical_reaction/slime/slimeglass/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimeglass/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/location = get_turf(holder.my_atom)
new /obj/item/stack/sheet/rglass(location, 5)
new /obj/item/stack/sheet/glass(location, 15)
@@ -91,7 +92,7 @@
required_other = TRUE
deletes_extract = FALSE //we do delete, but we don't do so instantly
-/datum/chemical_reaction/slime/slimemobspawn/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimemobspawn/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
summon_mobs(holder, T)
var/obj/item/slime_extract/M = holder.my_atom
@@ -131,7 +132,7 @@
required_container = /obj/item/slime_extract/silver
required_other = TRUE
-/datum/chemical_reaction/slime/slimebork/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimebork/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
//BORK BORK BORK
var/turf/T = get_turf(holder.my_atom)
@@ -173,7 +174,7 @@
required_container = /obj/item/slime_extract/blue
required_other = TRUE
-/datum/chemical_reaction/slime/slimestabilizer/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimestabilizer/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/stabilizer(get_turf(holder.my_atom))
..()
@@ -182,7 +183,7 @@
required_container = /obj/item/slime_extract/blue
required_other = TRUE
-/datum/chemical_reaction/slime/slimefoam/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimefoam/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
holder.create_foam(/datum/effect_system/foam_spread,80, "[src] spews out foam!")
//Dark Blue
@@ -192,7 +193,7 @@
required_other = TRUE
deletes_extract = FALSE
-/datum/chemical_reaction/slime/slimefreeze/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimefreeze/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
T.visible_message("The slime extract starts to feel extremely cold!")
addtimer(CALLBACK(src, .proc/freeze, holder), 50)
@@ -213,7 +214,7 @@
required_container = /obj/item/slime_extract/darkblue
required_other = TRUE
-/datum/chemical_reaction/slime/slimefireproof/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimefireproof/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/fireproof(get_turf(holder.my_atom))
..()
@@ -230,7 +231,7 @@
required_other = TRUE
deletes_extract = FALSE
-/datum/chemical_reaction/slime/slimefire/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimefire/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
T.visible_message("The slime extract begins to vibrate adorably!")
addtimer(CALLBACK(src, .proc/slime_burn, holder), 50)
@@ -258,7 +259,7 @@
required_container = /obj/item/slime_extract/yellow
required_other = TRUE
-/datum/chemical_reaction/slime/slimeoverload/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slimeoverload/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
empulse(get_turf(holder.my_atom), 3, 7)
..()
@@ -267,7 +268,7 @@
required_container = /obj/item/slime_extract/yellow
required_other = TRUE
-/datum/chemical_reaction/slime/slimeglow/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimeglow/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
T.visible_message("The slime begins to emit a soft light. Squeezing it will cause it to grow brightly.")
new /obj/item/flashlight/slime(T)
@@ -279,7 +280,7 @@
required_container = /obj/item/slime_extract/purple
required_other = TRUE
-/datum/chemical_reaction/slime/slimepsteroid/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimepsteroid/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/steroid(get_turf(holder.my_atom))
..()
@@ -295,7 +296,7 @@
required_container = /obj/item/slime_extract/darkpurple
required_other = TRUE
-/datum/chemical_reaction/slime/slimeplasma/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimeplasma/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/stack/sheet/mineral/plasma(get_turf(holder.my_atom), 3)
..()
@@ -305,7 +306,7 @@
required_container = /obj/item/slime_extract/red
required_other = TRUE
-/datum/chemical_reaction/slime/slimemutator/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimemutator/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/mutator(get_turf(holder.my_atom))
..()
@@ -314,7 +315,7 @@
required_container = /obj/item/slime_extract/red
required_other = TRUE
-/datum/chemical_reaction/slime/slimebloodlust/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimebloodlust/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
for(var/mob/living/simple_animal/slime/slime in viewers(get_turf(holder.my_atom), null))
if(slime.docile) //Undoes docility, but doesn't make rabid.
slime.visible_message("[slime] forgets its training, becoming wild once again!")
@@ -330,7 +331,7 @@
required_container = /obj/item/slime_extract/red
required_other = TRUE
-/datum/chemical_reaction/slime/slimespeed/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimespeed/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/speed(get_turf(holder.my_atom))
..()
@@ -340,7 +341,7 @@
required_container = /obj/item/slime_extract/pink
required_other = TRUE
-/datum/chemical_reaction/slime/docility/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/docility/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/docility(get_turf(holder.my_atom))
..()
@@ -349,7 +350,7 @@
required_container = /obj/item/slime_extract/pink
required_other = TRUE
-/datum/chemical_reaction/slime/gender/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/gender/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/genderchange(get_turf(holder.my_atom))
..()
@@ -367,7 +368,7 @@
required_other = TRUE
deletes_extract = FALSE
-/datum/chemical_reaction/slime/slimeexplosion/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimeexplosion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
var/lastkey = holder.my_atom.fingerprintslast
var/touch_msg = "N/A"
@@ -400,7 +401,7 @@
required_reagents = list(/datum/reagent/toxin/plasma = 1)
required_other = TRUE
-/datum/chemical_reaction/slime/slimepotion2/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimepotion2/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/sentience(get_turf(holder.my_atom))
..()
@@ -409,7 +410,7 @@
required_reagents = list(/datum/reagent/water = 1)
required_other = TRUE
-/datum/chemical_reaction/slime/renaming/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/renaming/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/renaming(holder.my_atom.drop_location())
..()
@@ -420,7 +421,7 @@
required_container = /obj/item/slime_extract/adamantine
required_other = TRUE
-/datum/chemical_reaction/slime/adamantine/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/adamantine/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/stack/sheet/mineral/adamantine(get_turf(holder.my_atom))
..()
@@ -430,7 +431,7 @@
required_container = /obj/item/slime_extract/bluespace
required_other = TRUE
-/datum/chemical_reaction/slime/slimefloor2/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slimefloor2/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/stack/tile/bluespace(get_turf(holder.my_atom), 25)
..()
@@ -440,7 +441,7 @@
required_container = /obj/item/slime_extract/bluespace
required_other = TRUE
-/datum/chemical_reaction/slime/slimecrystal/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slimecrystal/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/obj/item/stack/ore/bluespace_crystal/BC = new (get_turf(holder.my_atom))
BC.visible_message("The [BC.name] appears out of thin air!")
..()
@@ -450,7 +451,7 @@
required_container = /obj/item/slime_extract/bluespace
required_other = TRUE
-/datum/chemical_reaction/slime/slimeradio/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slimeradio/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/slime/slimeradio(get_turf(holder.my_atom))
..()
@@ -460,7 +461,7 @@
required_container = /obj/item/slime_extract/cerulean
required_other = TRUE
-/datum/chemical_reaction/slime/slimepsteroid2/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimepsteroid2/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/enhancer(get_turf(holder.my_atom))
..()
@@ -469,7 +470,7 @@
required_container = /obj/item/slime_extract/cerulean
required_other = TRUE
-/datum/chemical_reaction/slime/slime_territory/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slime_territory/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/areaeditor/blueprints/slime(get_turf(holder.my_atom))
..()
@@ -479,7 +480,7 @@
required_container = /obj/item/slime_extract/sepia
required_other = TRUE
-/datum/chemical_reaction/slime/slimestop/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimestop/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
addtimer(CALLBACK(src, .proc/slime_stop, holder), 5 SECONDS)
/datum/chemical_reaction/slime/slimestop/proc/slime_stop(datum/reagents/holder)
@@ -498,7 +499,7 @@
required_container = /obj/item/slime_extract/sepia
required_other = TRUE
-/datum/chemical_reaction/slime/slimecamera/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimecamera/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/camera(get_turf(holder.my_atom))
new /obj/item/camera_film(get_turf(holder.my_atom))
..()
@@ -508,7 +509,7 @@
required_container = /obj/item/slime_extract/sepia
required_other = TRUE
-/datum/chemical_reaction/slime/slimefloor/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimefloor/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/stack/tile/sepia(get_turf(holder.my_atom), 25)
..()
@@ -518,7 +519,7 @@
required_container = /obj/item/slime_extract/pyrite
required_other = TRUE
-/datum/chemical_reaction/slime/slimepaint/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimepaint/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/chosen = pick(subtypesof(/obj/item/paint))
new chosen(get_turf(holder.my_atom))
..()
@@ -528,7 +529,7 @@
required_container = /obj/item/slime_extract/pyrite
required_other = TRUE
-/datum/chemical_reaction/slime/slimecrayon/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slimecrayon/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/chosen = pick(difflist(subtypesof(/obj/item/toy/crayon),typesof(/obj/item/toy/crayon/spraycan)))
new chosen(get_turf(holder.my_atom))
..()
@@ -539,7 +540,7 @@
required_other = TRUE
required_container = /obj/item/slime_extract/rainbow
-/datum/chemical_reaction/slime/slime_rng/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slime_rng/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
if(created_volume >= 5)
var/obj/item/grenade/clusterbuster/slime/S = new (get_turf(holder.my_atom))
S.visible_message("Infused with plasma, the core begins to expand uncontrollably!")
@@ -556,7 +557,7 @@
required_other = TRUE
required_container = /obj/item/slime_extract/rainbow
-/datum/chemical_reaction/slime/slimebomb/on_reaction(datum/reagents/holder, created_volume)
+/datum/chemical_reaction/slime/slimebomb/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
var/turf/T = get_turf(holder.my_atom)
var/obj/item/grenade/clusterbuster/slime/volatile/S = new (T)
S.visible_message("Infused with slime jelly, the core begins to expand uncontrollably!")
@@ -577,7 +578,7 @@
required_other = TRUE
required_container = /obj/item/slime_extract/rainbow
-/datum/chemical_reaction/slime/slime_transfer/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/slime_transfer/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/slimepotion/transference(get_turf(holder.my_atom))
..()
@@ -586,6 +587,6 @@
required_other = TRUE
required_container = /obj/item/slime_extract/rainbow
-/datum/chemical_reaction/slime/flight_potion/on_reaction(datum/reagents/holder)
+/datum/chemical_reaction/slime/flight_potion/on_reaction(datum/equilibrium/reaction, datum/reagents/holder, created_volume)
new /obj/item/reagent_containers/glass/bottle/potion/flight(get_turf(holder.my_atom))
..()
diff --git a/code/modules/reagents/chemistry/recipes/special.dm b/code/modules/reagents/chemistry/recipes/special.dm
index f257ea821411dc..a016d4c4b65095 100644
--- a/code/modules/reagents/chemistry/recipes/special.dm
+++ b/code/modules/reagents/chemistry/recipes/special.dm
@@ -34,6 +34,13 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
/datum/chemical_reaction/randomized
+ //Increase default leniency because these are already hard enough
+ optimal_ph_min = 1
+ optimal_ph_max = 13
+ temp_exponent_factor = 0
+ ph_exponent_factor = 1
+ H_ion_release = 0
+
var/persistent = FALSE
var/persistence_period = 7 //Will reset every x days
var/created //creation timestamp
@@ -44,6 +51,22 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
var/randomize_req_temperature = TRUE
var/min_temp = 1
var/max_temp = 600
+ ///If the reaction can be exothermic or endothermic randomly too
+ var/exo_or_endothermic = FALSE
+
+ ///If pH is randomised
+ var/randomize_req_ph = FALSE
+ ///Lowest pH value possible
+ var/min_ph = 0
+ ///Highest pH value possible
+ var/max_ph = 14
+ ///How much the range can deviate, and also affects impure range
+ var/inoptimal_range_ph = 3
+
+ ///If the impurity failure threshold is randomized between 0 - 0.4
+ var/randomize_impurity_minimum = FALSE
+ ///If the impure/inverse/failure reagents are randomized
+ var/randomize_impurity_reagents = FALSE
var/randomize_inputs = TRUE
var/min_input_reagent_amount = 1
@@ -67,8 +90,39 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
if(randomize_container)
required_container = pick(possible_containers)
if(randomize_req_temperature)
- required_temp = rand(min_temp,max_temp)
is_cold_recipe = pick(TRUE,FALSE)
+ if(is_cold_recipe)
+ required_temp = rand(min_temp+50, max_temp)
+ optimal_temp = rand(min_temp+25, required_temp-10)
+ overheat_temp = rand(min_temp, optimal_temp-10)
+ if(overheat_temp >= 200) //Otherwise it can disappear when you're mixing and I don't want this to happen here
+ overheat_temp = 200
+ if(exo_or_endothermic)
+ thermic_constant = (rand(-200, 200))
+ else
+ required_temp = rand(min_temp, max_temp-50)
+ optimal_temp = rand(required_temp+10, max_temp-25)
+ overheat_temp = rand(optimal_temp, max_temp+50)
+ if(overheat_temp <= 400)
+ overheat_temp = 400
+ if(exo_or_endothermic)
+ thermic_constant = (rand(-200, 200))
+
+ if(randomize_req_ph)
+ optimal_ph_min = min_ph + rand(0, inoptimal_range_ph)
+ optimal_ph_max = max((max_ph + rand(0, inoptimal_range_ph)), (min_ph + 1)) //Always ensure we've a window of 1
+ determin_ph_range = inoptimal_range_ph
+ H_ion_release = (rand(0, 25)/100)// 0 - 0.25
+
+ if(randomize_impurity_minimum)
+ purity_min = (rand(0, 4)/10)
+
+ if(randomize_impurity_reagents)
+ for(var/rid in required_reagents)
+ var/datum/reagent/R = GLOB.chemical_reagents_list[rid]
+ R.impure_chem = get_random_reagent_id()
+ R.inverse_chem = get_random_reagent_id()
+ R.failed_chem = get_random_reagent_id()
if(randomize_results)
results = list()
@@ -141,8 +195,18 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
return FALSE
required_catalysts = req_catalysts
- required_temp = recipe_data["required_temp"]
is_cold_recipe = recipe_data["is_cold_recipe"]
+ required_temp = recipe_data["required_temp"]
+ optimal_temp = recipe_data["optimal_temp"]
+ overheat_temp = recipe_data["overheat_temp"]
+ thermic_constant = recipe_data["thermic_constant"]
+
+ optimal_ph_min = recipe_data["optimal_ph_min"]
+ optimal_ph_max = recipe_data["optimal_ph_max"]
+ determin_ph_range = recipe_data["determin_ph_range"]
+ H_ion_release = recipe_data["H_ion_release"]
+
+ purity_min = recipe_data["purity_min"]
var/temp_results = unwrap_reagent_list(recipe_data["results"])
if(!temp_results)
@@ -226,15 +290,28 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
var/datum/reagent/R = GLOB.chemical_reagents_list[rid]
dat += "
[recipe.required_catalysts[rid]]u of [R.name]
"
dat += ""
- dat += "Mix slowly"
+ dat += "Mix slowly
"
if(recipe.required_container)
var/obj/item/I = recipe.required_container
- dat += " in [initial(I.name)]"
+ dat += "
in a [initial(I.name)]
"
if(recipe.required_temp != 0)
if(recipe.is_cold_recipe)
- dat += " below [recipe.required_temp] degrees"
+ dat += "
cooling it below [recipe.required_temp] degrees"
+ dat += " but not below [recipe.overheat_temp] degrees
"
else
- dat += " above [recipe.required_temp] degrees"
+ dat += "
heating it above [recipe.required_temp] degrees"
+ dat += " but not above [recipe.overheat_temp] degrees"
+ if(recipe.thermic_constant > 0)
+ dat += "
taking care of it's exothermic nature
"
+ else if(recipe.thermic_constant < 0)
+ dat += "