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
🆑
code: adds a framework for implementing effects when multiple reagents
are metabolized per tick
/🆑

---------

Co-authored-by: Jordan Dominion <jordanhcbrown+github@gmail.com>
This commit is contained in:
SyncIt21
2026-02-19 15:22:57 +01:00
committed by GitHub
co-authored by Jordan Dominion
parent 7e3bc5695e
commit 2eb4b5e0cb
6 changed files with 108 additions and 5 deletions
+8
View File
@@ -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()
@@ -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
@@ -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
+1
View File
@@ -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"
+22
View File
@@ -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)
+1
View File
@@ -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"