From 2eb4b5e0cbd3490699d34f4e9333b386c798a36f Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:52:57 +0530 Subject: [PATCH] Adds the framework for stacked metabolization effects (#95123) ## About The Pull Request - You can now add effects when multiple reagents are metabolized per tick by creating a new subtype of `/datum/stacked_metabolization_effect`. So lets say you have 3 reagents A & B & C and you want to implement some unique effect when all 3 reagents are present at the same time. Instead of implementing those effect inside each of those reagents like such ```dm /datum/reagentA/on_mob_life() if(holder.hasReagent(/datum/reagentB) && holder.hasReagent(/datum/reagentC)) //do stuff /datum/reagentB/on_mob_life() if(holder.hasReagent(/datum/reagentC) && holder.hasReagent(/datum/reagentA)) //do stuff /datum/reagentC/on_mob_life() if(holder.hasReagent(/datum/reagentA) && holder.hasReagent(/datum/reagentB)) //do stuff ``` You can now implement that effect by creating a subtype of stack reagent effect as such ```dm /datum/stacked_metabolization_effect/unique_effect requirements = list(/datum/reagent/A = 1, /datum/reagent/B = 1, /datum/reagent/C = 1) /datum/stacked_metabolization_effect/unique_effect/apply(list/reagents_metabolized, mob/living/carbon/owner, seconds_per_tick) var/metabolization_ratio = average(reagents_metabolized) //do stuff with the ratio ``` The effect is applied per tick and you can check for subtypes as well like such ```dm /datum/stacked_metabolization_effect/unique_effect requirements = list(/datum/reagent/A = 3) ``` ## Why it's good for the game The framework of stacked reagent effects allows contributors to implement unique effects for metabolizing multiple reagents at once. The benefits are obvious. Right now it's a framework that is unused but should hopefully see some use in the future ## Changelog :cl: code: adds a framework for implementing effects when multiple reagents are metabolized per tick /:cl: --------- Co-authored-by: Jordan Dominion --- code/_globalvars/lists/reagents.dm | 8 +++ .../reagents/chemistry/holder/mob_life.dm | 20 ++++-- .../stacked_effects/stacked_effects.dm | 61 +++++++++++++++++++ code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/stacked_metab.dm | 22 +++++++ tgstation.dme | 1 + 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 code/modules/reagents/stacked_effects/stacked_effects.dm create mode 100644 code/modules/unit_tests/stacked_metab.dm diff --git a/code/_globalvars/lists/reagents.dm b/code/_globalvars/lists/reagents.dm index 225b12c8353..8aeacb6f583 100644 --- a/code/_globalvars/lists/reagents.dm +++ b/code/_globalvars/lists/reagents.dm @@ -58,6 +58,8 @@ GLOBAL_LIST_INIT(blacklisted_metalgen_types, typecacheof(list( GLOBAL_LIST_INIT(name2reagent, build_name2reagentlist()) /// list of all plan traits GLOBAL_LIST_INIT(plant_traits, init_plant_traits()) +/// List of all reagent side effects +GLOBAL_LIST_INIT(stacked_metabolization_effect, init_chemical_side_effects()) /// Initialises all /datum/reagent into a list indexed by reagent id /proc/init_chemical_reagent_list() @@ -198,3 +200,9 @@ GLOBAL_LIST_INIT(plant_traits, init_plant_traits()) //build map with sorted keys for(var/name in only_names) .[name] = name_to_reagent[name] + +/proc/init_chemical_side_effects() + . = list() + + for(var/datum/stacked_metabolization_effect/effect as anything in valid_subtypesof(/datum/stacked_metabolization_effect)) + . += new effect() diff --git a/code/modules/reagents/chemistry/holder/mob_life.dm b/code/modules/reagents/chemistry/holder/mob_life.dm index 978f1fb0d99..693dc8bb885 100644 --- a/code/modules/reagents/chemistry/holder/mob_life.dm +++ b/code/modules/reagents/chemistry/holder/mob_life.dm @@ -30,6 +30,7 @@ liver_tolerance = liver.toxTolerance * liver_health_percent provide_pain_message = HAS_NO_TOXIN + var/list/reagents_metabolized = list() for(var/datum/reagent/reagent as anything in cached_reagents) var/datum/reagent/toxin/toxin if(istype(reagent, /datum/reagent/toxin)) @@ -44,7 +45,7 @@ owner.reagents.remove_reagent(toxin.type, toxin.metabolization_rate * owner.metabolism_efficiency * seconds_per_tick) continue - need_mob_update += metabolize_reagent(owner, reagent, seconds_per_tick, can_overdose, liverless, dead) + need_mob_update += metabolize_reagent(owner, reagent, seconds_per_tick, can_overdose, liverless, dead, reagents_metabolized) // If applicable, calculate any toxin-related liver damage // Note: we have to do this AFTER metabolize_reagent, because we want handle_reagent to run before we make the determination. @@ -66,8 +67,15 @@ if(provide_pain_message && liver.damage > 10 && SPT_PROB(liver.damage/6, seconds_per_tick)) //the higher the damage the higher the probability to_chat(owner, span_warning("You feel a dull pain in your abdomen.")) - if(owner && need_mob_update) //some of the metabolized reagents had effects on the mob that requires some updates. - owner.updatehealth() + if(owner) + //apply side effects based on reagents metabolized + if(reagents_metabolized.len > 1) + for(var/datum/stacked_metabolization_effect/effect as anything in GLOB.stacked_metabolization_effect) + need_mob_update += effect.check_and_apply(reagents_metabolized, owner, seconds_per_tick) + + //some of the metabolized reagents had effects on the mob that requires some updates. + if(need_mob_update) + owner.updatehealth() #undef HAS_SILENT_TOXIN #undef HAS_NO_TOXIN @@ -80,11 +88,11 @@ * Arguments: * * mob/living/carbon/owner - The mob to metabolize in, if null it uses [/datum/reagents/var/my_atom] * * seconds_per_tick - the time in server seconds between proc calls (when performing normally it will be 2) - * * times_fired - the number of times the owner's life() tick has been called aka The number of times SSmobs has fired * * can_overdose - Allows overdosing * * liverless - Stops reagents that aren't set as [/datum/reagent/var/self_consuming] from metabolizing + * * list/reagents_metabolized - a list to hold the reagent metabolized with the metabolization ratio */ -/datum/reagents/proc/metabolize_reagent(mob/living/carbon/owner, datum/reagent/reagent, seconds_per_tick, can_overdose = FALSE, liverless = FALSE, dead = FALSE) +/datum/reagents/proc/metabolize_reagent(mob/living/carbon/owner, datum/reagent/reagent, seconds_per_tick, can_overdose = FALSE, liverless = FALSE, dead = FALSE, list/reagents_metabolized) if(QDELETED(reagent.holder)) return FALSE @@ -104,6 +112,8 @@ var/need_mob_update = FALSE var/metabolized_volume = reagent.compute_metabolization(owner, seconds_per_tick) var/metabolization_ratio = REM * metabolized_volume + if(reagents_metabolized) + reagents_metabolized[reagent.type] = metabolization_ratio if(can_overdose && !HAS_TRAIT(owner, TRAIT_OVERDOSEIMMUNE)) if(reagent.overdose_threshold && reagent.volume >= reagent.overdose_threshold && !reagent.overdosed) reagent.overdosed = TRUE diff --git a/code/modules/reagents/stacked_effects/stacked_effects.dm b/code/modules/reagents/stacked_effects/stacked_effects.dm new file mode 100644 index 00000000000..c1e76e1857d --- /dev/null +++ b/code/modules/reagents/stacked_effects/stacked_effects.dm @@ -0,0 +1,61 @@ +///Side effects from metabolizing an reagent or a combination of them +/datum/stacked_metabolization_effect + abstract_type = /datum/stacked_metabolization_effect + ///List of reagents that need to be metabolized for this side effect to kick in. For subtypes values greater than requirement list will also trigger this effect + var/list/datum/reagent/requirements + +/** + * Returns the average metabolzation ratio from an list + * Arguments + * + * * list/reagents_metabolized - the list of reagents metabolized +*/ +/datum/stacked_metabolization_effect/proc/average(list/reagents_metabolized) + SHOULD_BE_PURE(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + + var/average = 0 + for(var/datum/reagent/test as anything in reagents_metabolized) + average += reagents_metabolized[test] + return average / reagents_metabolized.len + +/** + * Checks if this side effect can be applied on the mob + * Arguments + * + * * list/reagents_metabolized - a map of reagent type path -> metabolization_ratio of all reagents + * * mob/living/carbon/owner - the mob to apply the side effects to + * * seconds_per_tick - passed from /datum/reagents/proc/metabolize_reagent() +*/ +/datum/stacked_metabolization_effect/proc/check_and_apply(list/reagents_metabolized, mob/living/carbon/owner, seconds_per_tick) + SHOULD_NOT_OVERRIDE(TRUE) + . = 0 + + var/list/datum/reagent/requirements_needed = requirements + for(var/datum/reagent/test as anything in reagents_metabolized) + for(var/datum/reagent/requirement as anything in requirements_needed) + if(ispath(test, requirement)) + if(requirements_needed == requirements) + requirements_needed = requirements.Copy() + requirements_needed[requirement] -= 1 + if(!requirements_needed[requirement]) + requirements_needed -= requirement + break + + if(!requirements_needed.len) + return apply(reagents_metabolized, owner, seconds_per_tick) + +/** + * Apply a list of side effects to an mob once they have metabolized the requirments + * Arguments + * + * * list/reagents_metabolized - a map of reagent -> metabolization_ratio of all reagents + * * mob/living/carbon/owner - the mob to apply the side effects to + * * seconds_per_tick - passed from /datum/reagents/proc/metabolize_reagent() + * Returns a positive value if the mobs health needs to be updated +*/ +/datum/stacked_metabolization_effect/proc/apply(list/reagents_metabolized, mob/living/carbon/owner, seconds_per_tick) + PROTECTED_PROC(TRUE) + SHOULD_NOT_SLEEP(TRUE) + + return 0 diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index b37ce733209..5894dc7feec 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -313,6 +313,7 @@ #include "spraycan.dm" #include "spritesheets.dm" #include "stack_singular_name.dm" +#include "stacked_metab.dm" #include "station_trait_tests.dm" #include "status_effect_validity.dm" #include "stomach.dm" diff --git a/code/modules/unit_tests/stacked_metab.dm b/code/modules/unit_tests/stacked_metab.dm new file mode 100644 index 00000000000..94158acb8f5 --- /dev/null +++ b/code/modules/unit_tests/stacked_metab.dm @@ -0,0 +1,22 @@ +/datum/unit_test/stacked_metabolization_effect_verify + +/datum/unit_test/stacked_metabolization_effect_verify/Run() + for(var/datum/stacked_metabolization_effect/effect as anything in valid_subtypesof(/datum/stacked_metabolization_effect)) + effect = new effect() + + if(!length(effect.requirements)) + TEST_FAIL("Effect [effect] does not have any requirments!") + qdel(effect) + continue + + for(var/datum/reagent/key as anything in effect.requirements) + if(!ispath(key, /datum/reagent)) + TEST_FAIL("Effect [effect] has an non reagent key [key] as requirment") + break + + var/count = effect.requirements[key] + if(count < 1) + TEST_FAIL("Effect [effect] has an invalid requirement count [count] for key [key] ") + break + + qdel(effect) diff --git a/tgstation.dme b/tgstation.dme index 303cf1ab8ce..492cfa5ca93 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6127,6 +6127,7 @@ #include "code\modules\reagents\reagent_containers\cups\mauna_mug.dm" #include "code\modules\reagents\reagent_containers\cups\organ_jar.dm" #include "code\modules\reagents\reagent_containers\cups\soda.dm" +#include "code\modules\reagents\stacked_effects\stacked_effects.dm" #include "code\modules\reagents\withdrawal\_addiction.dm" #include "code\modules\reagents\withdrawal\generic_addictions.dm" #include "code\modules\recycling\conveyor.dm"