diff --git a/baystation12.dme b/baystation12.dme
index f9e87ce0049c4..cbc43ffe2b6b0 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -494,6 +494,7 @@
#include "code\datums\traits\general\nonpermeable_skin.dm"
#include "code\datums\traits\general\permeable_skin.dm"
#include "code\datums\traits\general\serpentid_adapted.dm"
+#include "code\datums\traits\maluses\allergy.dm"
#include "code\datums\traits\maluses\animal_protein.dm"
#include "code\datums\traits\maluses\ethanol.dm"
#include "code\datums\traits\maluses\malus.dm"
@@ -2166,6 +2167,7 @@
#include "code\modules\mob\living\bot\mulebot.dm"
#include "code\modules\mob\living\bot\remotebot.dm"
#include "code\modules\mob\living\bot\secbot.dm"
+#include "code\modules\mob\living\carbon\allergy.dm"
#include "code\modules\mob\living\carbon\breathe.dm"
#include "code\modules\mob\living\carbon\carbon.dm"
#include "code\modules\mob\living\carbon\carbon_defense.dm"
diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm
index 46e417f7f1cf1..bd13be60a217a 100644
--- a/code/__defines/mobs.dm
+++ b/code/__defines/mobs.dm
@@ -496,3 +496,7 @@
/// Integer (~ticks * SSMobs/wait fire rate). The default maximum value a mob's confused var can be set to.
#define CONFUSED_MAX 15
+
+///Flags assigned to carbon mobs trait_flags when they're actively having an allergy.
+#define MILD_ALLERGY FLAG(0)
+#define SEVERE_ALLERGY FLAG(1)
\ No newline at end of file
diff --git a/code/datums/traits/_defines.dm b/code/datums/traits/_defines.dm
index 4c83db5cec464..592c55a970ef0 100644
--- a/code/datums/traits/_defines.dm
+++ b/code/datums/traits/_defines.dm
@@ -1,6 +1,9 @@
// Helpers for shorter trait code
+///Check if mob currently has a trait set; also works for traits with associated_list set.
#define HAS_TRAIT(MOB, TRAIT) MOB.HasTrait(TRAIT)
+///Checks for minimum severity level with associated trait. Does not work for traits that have an metaoptions set.
#define HAS_TRAIT_LEVEL(MOB, TRAIT, LEVEL) (M.GetTraitLevel(TRAIT) >= LEVEL)
+///Gets severity level with associated trait. Does not work for traits that have an metaoptions set.
#define GET_TRAIT_LEVEL(MOB, TRAIT) M.GetTraitLevel(TRAIT)
#define IS_METABOLICALLY_INERT(MOB) HAS_TRAIT(MOB, /singleton/trait/general/metabolically_inert) // This define exists only due to how common this check is
#define METABOLIC_INERTNESS(MOB) GET_TRAIT_LEVEL(MOB, /singleton/trait/general/metabolically_inert) // See above
diff --git a/code/datums/traits/maluses/allergy.dm b/code/datums/traits/maluses/allergy.dm
new file mode 100644
index 0000000000000..c3a1c81dc2e30
--- /dev/null
+++ b/code/datums/traits/maluses/allergy.dm
@@ -0,0 +1,38 @@
+/singleton/trait/malus/allergy
+ name = "Allergy"
+ levels = list(TRAIT_LEVEL_MINOR, TRAIT_LEVEL_MAJOR)
+ ///Used to select which reagent mob is allergic to.
+ metaoptions = list(
+ /datum/reagent/antidexafen,
+ /datum/reagent/bicaridine,
+ /datum/reagent/citalopram,
+ /datum/reagent/dermaline,
+ /datum/reagent/drink/juice/apple,
+ /datum/reagent/drink/juice/berry,
+ /datum/reagent/drink/juice/garlic,
+ /datum/reagent/drink/juice/orange,
+ /datum/reagent/drink/kefir,
+ /datum/reagent/drink/thoom,
+ /datum/reagent/drugs/psilocybin,
+ /datum/reagent/drugs/three_eye,
+ /datum/reagent/ethanol/creme_de_menthe,
+ /datum/reagent/ethanol/gin,
+ /datum/reagent/ethanol/tequilla,
+ /datum/reagent/ethanol/vodka,
+ /datum/reagent/hyperzine,
+ /datum/reagent/kelotane,
+ /datum/reagent/nanoblood,
+ /datum/reagent/paracetamol,
+ /datum/reagent/paroxetine,
+ /datum/reagent/peridaxon,
+ /datum/reagent/spaceacillin,
+ /datum/reagent/tramadol,
+ /datum/reagent/tramadol/oxycodone,
+ /datum/reagent/tricordrazine,
+ /datum/reagent/toxin/amatoxin,
+ /datum/reagent/toxin/carpotoxin,
+ /datum/reagent/toxin/venom
+ )
+ addprompt = "Select reagent to make mob allergic to."
+ remprompt = "Select reagent to remove allergy to."
+ selectable = TRUE
diff --git a/code/datums/traits/maluses/animal_protein.dm b/code/datums/traits/maluses/animal_protein.dm
index 294c7b4aa3833..dc36a3caa6e50 100644
--- a/code/datums/traits/maluses/animal_protein.dm
+++ b/code/datums/traits/maluses/animal_protein.dm
@@ -1,3 +1,3 @@
/singleton/trait/malus/animal_protein
- name = "Animal Protein Allergy"
+ name = "Animal Protein Intolerance"
levels = list(TRAIT_LEVEL_MINOR, TRAIT_LEVEL_MAJOR)
diff --git a/code/datums/traits/maluses/ethanol.dm b/code/datums/traits/maluses/ethanol.dm
index 77f974a855bc3..02f34ddb37cf1 100644
--- a/code/datums/traits/maluses/ethanol.dm
+++ b/code/datums/traits/maluses/ethanol.dm
@@ -1,3 +1,3 @@
/singleton/trait/malus/ethanol
- name = "Ethanol Allergy"
+ name = "Ethanol Intolerance"
levels = list(TRAIT_LEVEL_MINOR, TRAIT_LEVEL_MODERATE, TRAIT_LEVEL_MAJOR)
diff --git a/code/datums/traits/maluses/sugar.dm b/code/datums/traits/maluses/sugar.dm
index 95cd924988e32..aec06a452c125 100644
--- a/code/datums/traits/maluses/sugar.dm
+++ b/code/datums/traits/maluses/sugar.dm
@@ -1,3 +1,3 @@
/singleton/trait/malus/sugar
- name = "Sugar Allergy"
+ name = "Sugar Intolerance"
levels = list(TRAIT_LEVEL_MINOR, TRAIT_LEVEL_MAJOR)
diff --git a/code/datums/traits/maluses/water.dm b/code/datums/traits/maluses/water.dm
index 6fc5f2f1d2877..42979c899c9ea 100644
--- a/code/datums/traits/maluses/water.dm
+++ b/code/datums/traits/maluses/water.dm
@@ -1,4 +1,3 @@
/singleton/trait/malus/water
- name = "Water Allergy"
- description = "Also known as aquagenic urticaria."
+ name = "Water Intolerance"
levels = list(TRAIT_LEVEL_MINOR, TRAIT_LEVEL_MODERATE, TRAIT_LEVEL_MAJOR)
diff --git a/code/datums/traits/traits.dm b/code/datums/traits/traits.dm
index b7298ddbe51c1..d7fbc88adf8b5 100644
--- a/code/datums/traits/traits.dm
+++ b/code/datums/traits/traits.dm
@@ -1,15 +1,24 @@
+
/mob/living/proc/HasTrait(trait_type)
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_NOT_SLEEP(TRUE)
return (trait_type in GetTraits())
-/mob/living/proc/GetTraitLevel(trait_type)
+/mob/living/proc/GetTraitLevel(trait_type, meta_option)
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_NOT_SLEEP(TRUE)
+ var/singleton/trait/trait = GET_SINGLETON(trait_type)
var/traits = GetTraits()
if(!traits)
return null
- return traits[trait_type]
+
+ if (length(trait.metaoptions))
+ if (!meta_option)
+ return
+ var/list/interim = traits[trait_type]
+ return interim[meta_option]
+
+ else return traits[trait_type]
/mob/living/proc/GetTraits()
SHOULD_NOT_SLEEP(TRUE)
@@ -21,24 +30,44 @@
return traits
return species.traits
-/mob/living/proc/SetTrait(trait_type, trait_level)
+/mob/living/proc/GetMetaOptions(trait_type)
+ RETURN_TYPE(/list)
+ if (!HasTrait(trait_type))
+ return
+ var/singleton/trait/trait = GET_SINGLETON(trait_type)
+ if (!trait.metaoptions)
+ return
+
+ return traits[trait_type]
+
+/mob/living/proc/SetTrait(trait_type, trait_level, meta_option)
SHOULD_NOT_SLEEP(TRUE)
- var/singleton/trait/T = GET_SINGLETON(trait_type)
- if(!T.Validate(trait_level))
+ var/singleton/trait/trait = GET_SINGLETON(trait_type)
+ if(!trait.Validate(trait_level, meta_option))
return FALSE
- if (!LAZYISIN(traits, trait_type))
- for (var/existing_trait_types in traits)
- var/singleton/trait/ET = GET_SINGLETON(existing_trait_types)
- if (trait_type in ET.incompatible_traits)
- return FALSE
-
- LAZYSET(traits, trait_type, trait_level)
+ for (var/existing_trait_types in traits)
+ var/singleton/trait/existing = GET_SINGLETON(existing_trait_types)
+ if (LAZYISIN(existing.incompatible_traits, trait_type) || LAZYISIN(trait.incompatible_traits, existing_trait_types))
+ return FALSE
+
+ if (length(trait.metaoptions))
+ var/list/interim = list()
+ if (!LAZYISIN(traits, trait_type))
+ LAZYSET(traits, trait_type, interim)
+
+ var/list/existing_meta_options = traits[trait_type]
+ if (existing_meta_options[meta_option] == trait_level)
+ return FALSE
+ LAZYSET(existing_meta_options, meta_option, trait_level)
+ LAZYSET(traits, trait_type, existing_meta_options)
+ else
+ LAZYSET(traits, trait_type, trait_level)
return TRUE
-/mob/living/carbon/human/SetTrait(trait_type, trait_level)
+/mob/living/carbon/human/SetTrait(trait_type, trait_level, additional_option)
var/singleton/trait/T = GET_SINGLETON(trait_type)
- if(!T.Validate(trait_level))
+ if(!T.Validate(trait_level, additional_option))
return FALSE
if(!traits) // If traits haven't been setup before, check if we need to do so now
@@ -47,35 +76,97 @@
return TRUE
traits = species.traits.Copy() // The setup is to simply copy the species list of traits
- return ..(trait_type, trait_level)
+ return ..(trait_type, trait_level, additional_option)
-/mob/living/proc/RemoveTrait(trait_type)
+/mob/living/proc/RemoveTrait(trait_type, additional_option)
+ if (additional_option)
+ var/list/interim = traits[trait_type]
+ LAZYREMOVE(interim, additional_option)
+ if (length(interim)) //If there remains other associations with the singleton, stop removing. Else; also remove the singleton.
+ return
LAZYREMOVE(traits, trait_type)
-/mob/living/carbon/human/RemoveTrait(trait_type)
+/mob/living/carbon/human/RemoveTrait(trait_type, additional_option)
// If traits haven't been setup, but we're trying to remove a trait that exists on the species then setup traits
if(!traits && (trait_type in species.traits))
traits = species.traits.Copy()
- ..(trait_type) // Could go through the trouble of nulling the traits list if it's again equal to the species list but eh
+ ..(trait_type, additional_option) // Could go through the trouble of nulling the traits list if it's again equal to the species list but eh
traits = traits || list() // But we do ensure that humans don't null their traits list, to avoid copying from species again
+/proc/LetterizeSeverity(severity)
+ switch (severity)
+ if (TRAIT_LEVEL_EXISTS)
+ severity = "Exists"
+ if (TRAIT_LEVEL_MINOR)
+ severity = "Minor"
+ if (TRAIT_LEVEL_MODERATE)
+ severity = "Moderate"
+ if (TRAIT_LEVEL_MAJOR)
+ severity = "Severe"
+ else
+ crash_with("Inappopriate arguments fed into proc.")
+ return severity
+
+/proc/sanitize_trait_prefs(list/preferences)
+ RETURN_TYPE(/list)
+ var/list/final_preferences = list()
+ if (isnull(preferences))
+ return list()
+ if (!islist(preferences))
+ crash_with("Inappropriate argument fed into proc.")
+ return
+ if (!length(preferences))
+ return list()
+
+ for (var/trait in preferences)
+ var/trait_type = istext(trait) ? text2path(trait) : trait
+ var/singleton/trait/selected = GET_SINGLETON(trait_type)
+ var/severity
+ if (length(selected.metaoptions))
+ var/list/interim = preferences[trait]
+ var/list/final_interim = list()
+ for (var/metaoption in interim)
+ var/metaoption_type = istext(metaoption) ? text2path(metaoption) : metaoption
+ severity = interim[metaoption]
+ LAZYSET(final_interim, metaoption_type, severity)
+ LAZYSET(final_preferences, trait_type, final_interim)
+
+ else
+ severity = preferences[trait]
+ LAZYSET(final_preferences, trait_type, severity)
+ return final_preferences
+
/singleton/trait
var/name
var/description
/// Should either only contain TRAIT_LEVEL_EXISTS or a set of the other TRAIT_LEVEL_* levels
var/list/levels = list(TRAIT_LEVEL_EXISTS)
+ /// Additional list with unique paths to associate singleton with. if needed. Currently used for reagents in allergies only.
+ var/list/metaoptions = list()
+ ///Prompts seen when adding/removing additional traits; only for traits with metaoptions set
+ var/addprompt = "Select a property to add."
+ var/remprompt = "Select a property to remove."
+
/// These trait types may not co-exist on the same mob/species
var/list/incompatible_traits
abstract_type = /singleton/trait
+ ///List of species in which this trait is forbidden.
+ var/list/forbidden_species = list()
+ ///Determines if trait can be selected in character setup
+ var/selectable = FALSE
+
/singleton/trait/New()
if(type == abstract_type)
CRASH("Invalid initialization")
-/singleton/trait/proc/Validate(level)
+/singleton/trait/proc/Validate(level, meta_option)
SHOULD_NOT_OVERRIDE(TRUE)
SHOULD_NOT_SLEEP(TRUE)
SHOULD_BE_PURE(TRUE)
- return (level in levels)
+ if (length(metaoptions))
+ return (level in levels) && (meta_option in metaoptions)
+ else
+ return (level in levels)
\ No newline at end of file
diff --git a/code/game/objects/items/weapons/storage/med_pouch.dm b/code/game/objects/items/weapons/storage/med_pouch.dm
index 6cb2541d5c95d..b0dd0bbba2795 100644
--- a/code/game/objects/items/weapons/storage/med_pouch.dm
+++ b/code/game/objects/items/weapons/storage/med_pouch.dm
@@ -215,3 +215,8 @@ Single Use Emergency Pouches
/obj/item/reagent_containers/hypospray/autoinjector/pouch_auto/adrenaline
name = "emergency adrenaline autoinjector"
starts_with = list(/datum/reagent/adrenaline = 5)
+
+/obj/item/reagent_containers/hypospray/autoinjector/pouch_auto/allergy
+ name = "emergency allergy autoinjector"
+ desc = "The ingredient label reads 1.5 units of epinephrine and 3.5 units of inaprovaline."
+ starts_with = list(/datum/reagent/adrenaline = 1.5, /datum/reagent/inaprovaline = 3.5)
\ No newline at end of file
diff --git a/code/modules/admin/view_variables/helpers.dm b/code/modules/admin/view_variables/helpers.dm
index 4dea5b57b3055..8b321562079cd 100644
--- a/code/modules/admin/view_variables/helpers.dm
+++ b/code/modules/admin/view_variables/helpers.dm
@@ -76,6 +76,8 @@
+
+
"}
/mob/living/carbon/human/get_view_variables_options()
diff --git a/code/modules/admin/view_variables/topic.dm b/code/modules/admin/view_variables/topic.dm
index 1c623573bde20..645372c70b561 100644
--- a/code/modules/admin/view_variables/topic.dm
+++ b/code/modules/admin/view_variables/topic.dm
@@ -576,6 +576,70 @@
var/mob/living/L = locate(href_list["debug_mob_ai"])
log_debug("AI Debugging toggled [L.ai_holder.debug() ? "ON" : "OFF"] for \the [L]")
+ else if (href_list["settrait"])
+ if (!check_rights(R_DEBUG|R_ADMIN|R_FUN)) return
+ var/mob/living/target = locate(href_list["settrait"])
+ if (!istype(target))
+ to_chat(usr, SPAN_WARNING("This can only be done to instances of /mob/living."))
+ return
+
+ var/list/trait_list = GET_SINGLETON_SUBTYPE_LIST(/singleton/trait)
+ var/singleton/trait/selected = input("Select a trait to apply to \the [target].", "Add Trait") as null | anything in trait_list
+
+ if (!selected || !istype(selected) || QDELETED(target))
+ return
+
+ var/selected_level
+ if (length(selected.levels) > 1)
+ var/list/letterized_levels = list()
+ for (var/severity in selected.levels)
+ LAZYSET(letterized_levels, LetterizeSeverity(severity), severity)
+ var/letter_level = input("Select the trait's level to apply to \the [target].", "Select Level") as null | anything in letterized_levels
+ selected_level = letterized_levels[letter_level]
+ else
+ selected_level = selected.levels[1]
+
+ if (QDELETED(target))
+ return
+
+ var/additional_data
+ if (length(selected.metaoptions))
+ var/list/sanitized_metaoptions
+ for (var/atom/option as anything in selected.metaoptions)
+ var/named_option = initial(option.name)
+ LAZYSET(sanitized_metaoptions, named_option, option)
+ var/sanitized_additional = input("[selected.addprompt]", "Select Option") as null | anything in sanitized_metaoptions
+ additional_data = sanitized_metaoptions[sanitized_additional]
+
+ if (target.SetTrait(selected.type, selected_level, additional_data))
+ to_chat(usr, SPAN_NOTICE("Successfuly set \the [selected.name] in \the [target]."))
+ else
+ to_chat(usr, SPAN_WARNING("Failed to set \the [selected.name] in \the [target]."))
+ return
+
+ else if (href_list["removetrait"])
+ if (!check_rights(R_DEBUG|R_ADMIN|R_FUN)) return
+ var/mob/living/target = locate(href_list["removetrait"])
+ if (!istype(target))
+ to_chat(usr, SPAN_WARNING("This can only be done to instances of /mob/living."))
+ return
+ var/input = input("Select a trait to remove from \the [target].", "Remove Trait") as null | anything in target.traits
+ var/singleton/trait/selected = GET_SINGLETON(input)
+ if (!selected || !istype(selected) || QDELETED(target))
+ return
+
+ var/additional_option
+ if (length(selected.metaoptions))
+ var/list/interim = target.traits[selected.type]
+ additional_option = input("[selected.remprompt]", "Select Option") as null | anything in interim
+ if (!additional_option)
+ return
+
+ target.RemoveTrait(selected.type, additional_option)
+ to_chat(usr, SPAN_NOTICE("Successfuly removed \the [selected.name] in \the [target]."))
+ return
+
+
else if (href_list["addmovementhandler"])
if (!check_rights(R_DEBUG))
return
diff --git a/code/modules/client/preference_setup/general/02_body.dm b/code/modules/client/preference_setup/general/02_body.dm
index 86c107ac890db..c14f7d845c9ce 100644
--- a/code/modules/client/preference_setup/general/02_body.dm
+++ b/code/modules/client/preference_setup/general/02_body.dm
@@ -21,6 +21,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
var/list/organ_data
var/list/rlimb_data
var/disabilities = 0
+ var/list/picked_traits
/datum/category_item/player_setup_item/physical/body
@@ -65,6 +66,8 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
pref.rlimb_data = R.read("rlimb_data")
pref.body_markings = R.read("body_markings")
pref.body_descriptors = R.read("body_descriptors")
+ pref.picked_traits = R.read("traits")
+ pref.picked_traits = sanitize_trait_prefs(pref.picked_traits)
/datum/category_item/player_setup_item/physical/body/save_character(datum/pref_record_writer/W)
@@ -86,6 +89,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
W.write("rlimb_data", pref.rlimb_data)
W.write("body_markings", pref.body_markings)
W.write("body_descriptors", pref.body_descriptors)
+ W.write("traits", pref.picked_traits)
/datum/category_item/player_setup_item/physical/body/sanitize_character()
@@ -114,6 +118,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
pref.disabilities = sanitize_integer(pref.disabilities, 0, 65535, initial(pref.disabilities))
if(!istype(pref.organ_data)) pref.organ_data = list()
if(!istype(pref.rlimb_data)) pref.rlimb_data = list()
+ if (!istype(pref.picked_traits)) pref.picked_traits = list()
if(!istype(pref.body_markings))
pref.body_markings = list()
else
@@ -239,6 +244,37 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
. += "
[alt_organs.Join(", ")]"
. = jointext(., null)
+ . += "
[TBTN("res_trait", "Reset Traits", "Traits")] [BTN("add_trait", "Add Trait")]"
+ var/list/alt_traits = list()
+ for (var/picked_type as anything in pref.picked_traits)
+ var/singleton/trait/picked = GET_SINGLETON(picked_type)
+ if (!picked || !istype(picked))
+ continue
+ var/name = picked.name
+ var/severity
+ if (length(picked.metaoptions))
+ var/list/metaoptions = pref.picked_traits[picked_type]
+ for (var/option as anything in metaoptions)
+ severity = metaoptions[option]
+ if (isnull(severity))
+ continue
+ severity = LetterizeSeverity(severity)
+ if (ispath(option, /datum/reagent))
+ var/datum/reagent/picked_reagent = option
+ option = initial(picked_reagent.name)
+ alt_traits += "[name] [option] [severity]"
+ else
+ severity = pref.picked_traits[picked_type]
+ if (isnull(severity))
+ continue
+ severity = LetterizeSeverity(severity)
+ alt_traits += "[name] [severity]"
+
+ if (!length(alt_traits))
+ alt_traits += "No traits selected."
+ . += "
[alt_traits.Join("; ")]"
+ . = jointext(., null)
+
/datum/category_item/player_setup_item/physical/body/proc/HasAppearanceFlag(datum/species/mob_species, flag)
return mob_species && (mob_species.appearance_flags & flag)
@@ -334,6 +370,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
reset_limbs() // Safety for species with incompatible manufacturers; easier than trying to do it case by case.
pref.body_markings.Cut() // Basically same as above.
+ pref.picked_traits.Cut()
prune_occupation_prefs()
pref.skills_allocated = pref.sanitize_skills(pref.skills_allocated)
@@ -642,6 +679,69 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
pref.disabilities ^= disability_flag
return TOPIC_REFRESH_UPDATE_PREVIEW
+ else if (href_list["res_trait"])
+ if (!length(pref.picked_traits))
+ return
+ pref.picked_traits.Cut()
+ return TOPIC_REFRESH
+
+ else if (href_list["add_trait"])
+ if (!mob_species)
+ return
+ var/list/possible_traits = mob_species.get_selectable_traits()
+ var/picked = input(user, "Select a trait to apply.", "Add Trait") as null | anything in possible_traits
+ var/singleton/trait/selected = possible_traits[picked]
+ if (!selected || !istype(selected))
+ return
+
+ var/list/possible_levels = selected.levels
+ var/selected_level
+ if (length(possible_levels) > 1)
+ var/list/letterized_levels
+ for (var/severity in possible_levels)
+ LAZYSET(letterized_levels, LetterizeSeverity(severity), severity)
+ var/letterized_input = input(user, "Select the trait's level to apply.", "Select Level") as null | anything in letterized_levels
+ selected_level = letterized_levels[letterized_input]
+ else
+ selected_level = possible_levels[1]
+
+ //[SIERRA-ADD] - pr34500 - Prevents runtimes, because as of now it can be null
+ if(!selected_level)
+ return
+ //[/SIERRA-ADD]
+
+ var/additional_data
+ if (length(selected.metaoptions))
+ var/list/sanitized_metaoptions
+ for (var/atom/option as anything in selected.metaoptions)
+ var/named_option = initial(option.name)
+ LAZYSET(sanitized_metaoptions, named_option, option)
+
+ var/additional_input = input(user, "[selected.addprompt]", "Select Option") as null | anything in sanitized_metaoptions
+ additional_data = sanitized_metaoptions[additional_input]
+
+ for (var/existing_type as anything in pref.picked_traits)
+ var/singleton/trait/existing_trait = GET_SINGLETON(existing_type)
+ if (!existing_trait || !istype(existing_trait))
+ continue
+ if (LAZYISIN(existing_trait.incompatible_traits, selected.type) || LAZYISIN(selected.incompatible_traits, existing_type))
+ to_chat(usr, SPAN_WARNING("The [selected.name] trait is incompatible with [existing_trait.name]."))
+ return
+
+ if (additional_data)
+ var/list/interim = list()
+ if (!LAZYISIN(pref.picked_traits, selected.type))
+ LAZYSET(pref.picked_traits, selected.type, interim)
+
+ var/list/existing_meta_options = pref.picked_traits[selected.type]
+ if (existing_meta_options[additional_data] == selected_level)
+ return
+ LAZYSET(existing_meta_options, additional_data, selected_level)
+ LAZYSET(pref.picked_traits, selected.type, existing_meta_options)
+ else
+ LAZYSET(pref.picked_traits, selected.type, selected_level)
+ return TOPIC_REFRESH
+
return ..()
diff --git a/code/modules/client/preference_setup/loadout/lists/misc.dm b/code/modules/client/preference_setup/loadout/lists/misc.dm
index 7611c1f5bf060..9d9bdc5dc1121 100644
--- a/code/modules/client/preference_setup/loadout/lists/misc.dm
+++ b/code/modules/client/preference_setup/loadout/lists/misc.dm
@@ -306,3 +306,9 @@
crosstype["cross, silver"] = /obj/item/material/cross/silver
crosstype["cross, gold"] = /obj/item/material/cross/gold
gear_tweaks += new/datum/gear_tweak/path(crosstype)
+
+/datum/gear/allergy_pen
+ display_name = "Allergy Autoinjector"
+ path = /obj/item/reagent_containers/hypospray/autoinjector/pouch_auto/allergy
+ cost = 1
+ allowed_traits = list(/singleton/trait/malus/allergy)
\ No newline at end of file
diff --git a/code/modules/client/preference_setup/loadout/loadout.dm b/code/modules/client/preference_setup/loadout/loadout.dm
index 54b0a1052749e..4ab8ab29f2da0 100644
--- a/code/modules/client/preference_setup/loadout/loadout.dm
+++ b/code/modules/client/preference_setup/loadout/loadout.dm
@@ -250,6 +250,22 @@ var/global/list/gear_datums = list()
entry += "[english_list(skill_checks)]"
+ if (allowed && G.allowed_traits)
+ var/datum/species/picked_species = all_species[pref.species]
+ var/list/species_traits = picked_species.traits
+ var/trait_checks = list()
+ entry += "
"
+ for (var/trait_type in G.allowed_traits)
+ var/singleton/trait/trait = GET_SINGLETON(trait_type)
+ var/trait_entry = "[trait.name]"
+ if (LAZYISIN(pref.picked_traits, trait_type) || LAZYISIN(species_traits, trait_type))
+ trait_entry = SPAN_COLOR("#55cc55", "[trait_entry]")
+ else
+ trait_entry = SPAN_COLOR("#cc5555", "[trait_entry]")
+ allowed = FALSE
+ trait_checks += trait_entry
+ entry += "[english_list(trait_checks)]"
+
// [SIERRA-ADD] - LOADOUT-ITEMS
if(allowed && G.allowed_factions)
var/good_background = 0
@@ -362,6 +378,8 @@ var/global/list/gear_datums = list()
var/list/allowed_roles //Roles that can spawn with this item.
var/list/allowed_branches //Service branches that can spawn with it.
var/list/allowed_skills //Skills required to spawn with this item.
+ ///Traits required to spawn with this item.
+ var/list/allowed_traits
var/whitelisted //Term to check the whitelist for..
var/sort_category = "General"
var/flags //Special tweaks in New
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index b9297d36a82ca..cb56e99ad9a12 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -364,6 +364,18 @@
character.gen_record = gen_record
character.exploit_record = exploit_record
+ if(LAZYLEN(picked_traits))
+ for (var/picked_type as anything in picked_traits)
+ var/singleton/trait/selected = GET_SINGLETON(picked_type)
+ if (!selected || !istype(selected))
+ continue
+ if (length(selected.metaoptions))
+ var/list/temp_list = picked_traits[picked_type]
+ for (var/meta_option in temp_list)
+ character.SetTrait(picked_type, temp_list[meta_option], meta_option)
+ else
+ character.SetTrait(picked_type, picked_traits[picked_type])
+
if(LAZYLEN(character.descriptors))
for(var/entry in body_descriptors)
character.descriptors[entry] = body_descriptors[entry]
diff --git a/code/modules/mob/living/carbon/allergy.dm b/code/modules/mob/living/carbon/allergy.dm
new file mode 100644
index 0000000000000..77262b3074388
--- /dev/null
+++ b/code/modules/mob/living/carbon/allergy.dm
@@ -0,0 +1,112 @@
+
+/* This file contains all the procs processing allergy onset, healing, and symptoms.
+For the file where the allergy trait is defined; check datum/traits/maluses/allergy.dm
+Mild allergies increase heart rate and give itch messages. Inaprovaline resolves these symptoms; but the allergy will keep running as long as a reagent is in the system.
+Severe allergies cause breathing problems and an even faster heart rate. Inaprovaline markedly stabilizes these symptoms; but only adrenaline can stop a severe allergy
+As long as you have inaprovaline in your system, an allergy cannot trigger. Key is to keep inaprov longer than the allergen exists above threshold after treatment */
+
+/*
+Checks if allergy will be triggered at a reagent level. Called by handle_allergy().
+If all offending reagent levels fall below threshold and no severe allergy is running; will stop allergies.
+Also checks if medications that stop allergies from triggering are in system. This is done after the list of active_allergies is updated.
+*/
+/mob/living/carbon/proc/check_allergy(datum/reagent/reagent, current_level = 0)
+ if (!ispath(reagent))
+ return
+ var/allergy_severity = GetTraitLevel(/singleton/trait/malus/allergy, reagent)
+ if (!allergy_severity)
+ return
+ var/threshold = 1/allergy_severity //For Medium sized mobs; threshold of 1 for minor allergies and 0.33 for major allergies.
+ if (current_level < threshold)
+ LAZYREMOVE(active_allergies, reagent)
+ return
+
+ LAZYDISTINCTADD(active_allergies, reagent)
+ start_allergy(allergy_severity)
+
+///This starts an allergy. Dosage/drug are handled at check_allergy proc, if you call this proc directly you must do your own checks.
+/mob/living/carbon/proc/start_allergy(allergy_severity)
+ if (trait_flags & SEVERE_ALLERGY)
+ return
+ if ((trait_flags & MILD_ALLERGY) && allergy_severity <= TRAIT_LEVEL_MINOR)
+ return
+ if (chem_effects[CE_STABLE] || chem_doses[/datum/reagent/adrenaline])
+ return
+
+ switch (allergy_severity)
+ if (TRAIT_LEVEL_MINOR)
+ trait_flags |= MILD_ALLERGY
+ to_chat(src, SPAN_DANGER("You start feeling uncontrollably itchy!"))
+ next_allergy_time = world.time + 2 MINUTES
+
+ if (TRAIT_LEVEL_MAJOR)
+ trait_flags |= SEVERE_ALLERGY
+ to_chat(src, SPAN_DANGER("Your throat starts swelling up and it suddenly becomes very difficult to breathe!"))
+ next_allergy_time = world.time + 1 MINUTES
+
+ else
+ crash_with("Allergy called with incorrect severity of [allergy_severity].")
+
+///Ends allergies and unsets flag. Conditions handled at handle_allergy proc; if you call this directly do your own checks.
+///If no severity supplied will end all allergies.
+/mob/living/carbon/proc/stop_allergy(allergy_flag)
+ if (!(trait_flags & (MILD_ALLERGY|SEVERE_ALLERGY)))
+ return
+
+ if ((trait_flags & MILD_ALLERGY) && (!allergy_flag || (allergy_flag & MILD_ALLERGY)))
+ if (!chem_effects[CE_STABLE]) //People with inaprov aren't itching to start with.
+ to_chat(src, SPAN_NOTICE("You feel the itching subside."))
+ trait_flags &= ~MILD_ALLERGY
+
+ if ((trait_flags & SEVERE_ALLERGY) && (!allergy_flag || (allergy_flag & SEVERE_ALLERGY)))
+ to_chat(src, SPAN_NOTICE("You feel your airways open up and breathing feels easier!"))
+ trait_flags &= ~SEVERE_ALLERGY
+
+/mob/living/var/next_allergy_time = 0
+/mob/living/proc/handle_allergy()
+ return
+
+///Main proc through which all other allergy procs are called; it is called by carbon/Life().
+///If adrenaline is in system, all active allergies will be stopped. Having inaprov (CE_STABLE) will prevent them from retriggering when adrenaline washes out.
+/mob/living/carbon/handle_allergy()
+ if (stat)
+ return
+ if (!HAS_TRAIT(src, /singleton/trait/malus/allergy))
+ return
+ var/list/allergy_list = traits[/singleton/trait/malus/allergy]
+
+ for (var/picked as anything in allergy_list)
+ if (!(picked in chem_doses) && !(picked in active_allergies))
+ continue
+ var/datum/reagent/reagent = picked
+ check_allergy(reagent, chem_doses[reagent])
+
+ if ((trait_flags & MILD_ALLERGY) && (!length(active_allergies)))
+ stop_allergy(MILD_ALLERGY)
+
+ if ((trait_flags & SEVERE_ALLERGY) && chem_doses[/datum/reagent/adrenaline] >= 1)
+ stop_allergy(SEVERE_ALLERGY)
+
+ run_allergy_symptoms()
+
+
+///Proc called by handle_allergy, handles chemical effects and symptoms.
+/mob/living/carbon/proc/run_allergy_symptoms()
+ if (!(trait_flags & (MILD_ALLERGY|SEVERE_ALLERGY)))
+ return
+
+ add_chemical_effect(CE_PULSE, 2)
+ if (trait_flags & SEVERE_ALLERGY)
+ add_chemical_effect(CE_BREATHLOSS, 2)
+ add_chemical_effect(CE_PULSE, 2)
+ if (prob(50))
+ add_chemical_effect(CE_VOICELOSS, 1)
+
+ if (!can_feel_pain() || world.time < next_allergy_time || chem_effects[CE_STABLE])
+ return
+
+ to_chat(src, SPAN_WARNING("You feel uncontrollably itchy!"))
+ var/delay = 2 MINUTES
+ if (trait_flags & SEVERE_ALLERGY)
+ delay /= 2
+ next_allergy_time = world.time + delay
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 5c7856226f33d..9853c78963a29 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -30,6 +30,7 @@
R.clear_reagents()
set_nutrition(400)
set_hydration(400)
+ stop_allergy()
..()
/mob/living/carbon/Move(NewLoc, direct)
diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm
index c0510b3a889e5..4c99a696b088c 100644
--- a/code/modules/mob/living/carbon/carbon_defines.dm
+++ b/code/modules/mob/living/carbon/carbon_defines.dm
@@ -40,3 +40,5 @@
var/stasis_value
var/player_triggered_sleeping = 0
+ ///Reagents towards which there is an active allergy.
+ var/list/active_allergies = list()
\ No newline at end of file
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index aab68c683bead..2052a0416ac8e 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -15,6 +15,8 @@
//Chemicals in the body
handle_chemicals_in_body()
+ handle_allergy()
+
//Random events (vomiting etc)
handle_random_events()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index ccfc073bbcf5d..b68582cc3956b 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -68,6 +68,8 @@
/// An associative list of /singleton/trait and trait level - See individual traits for valid levels
var/list/traits
+ /// Flags set by traits triggering behavior; currently used for allergies.
+ var/trait_flags
/// Some combination of HAZARD_FLAG_*. When set, the flagged hazard types will not damage the mob.
var/ignore_hazard_flags = EMPTY_BITFIELD
diff --git a/code/modules/modular_computers/file_system/reports/crew_record.dm b/code/modules/modular_computers/file_system/reports/crew_record.dm
index b11394bb97af8..75d942282ce2d 100644
--- a/code/modules/modular_computers/file_system/reports/crew_record.dm
+++ b/code/modules/modular_computers/file_system/reports/crew_record.dm
@@ -64,6 +64,19 @@ GLOBAL_VAR_INIT(arrest_security_status, "Arrest")
set_medRecord((H && H.med_record && !jobban_isbanned(H, "Records") ? html_decode(H.med_record) : "No record supplied"))
if(H)
+ if (H.HasTrait(/singleton/trait/malus/allergy))
+ var/list/allergies = H.GetMetaOptions(/singleton/trait/malus/allergy)
+ var/list/allergy_data = list()
+ var/severity
+ for (var/datum/reagent/picked as anything in allergies)
+ severity = allergies[picked]
+ if (isnull(severity))
+ continue
+ severity = LetterizeSeverity(severity)
+ allergy_data += "[severity] allergy to [initial(picked.name)]"
+ set_allergies(LAZYLEN(allergy_data)? jointext(allergy_data, "\[*\]") : "No allergies on record")
+
+
if(H.isSynthetic())
var/organ_data = list("Fully synthetic body")
for(var/obj/item/organ/internal/augment/A in H.internal_organs)
@@ -219,6 +232,7 @@ FIELD_LONG("General Notes (Public)", public_record, null, access_bridge)
FIELD_LIST("Blood Type", bloodtype, GLOB.blood_types, access_medical, access_medical)
FIELD_LONG("Medical Record", medRecord, access_medical, access_medical)
FIELD_LONG("Known Implants", implants, access_medical, access_medical)
+FIELD_LONG("Allergies", allergies, access_medical, access_medical)
// SECURITY RECORDS
FIELD_LIST("Criminal Status", criminalStatus, GLOB.security_statuses, access_security, access_security)
diff --git a/code/modules/species/species.dm b/code/modules/species/species.dm
index 0a31337de38cd..dc9ab272f5e55 100644
--- a/code/modules/species/species.dm
+++ b/code/modules/species/species.dm
@@ -282,7 +282,7 @@
/// When being fed a reagent item, the amount this species eats per bite on help intent.
var/ingest_amount = 10
- /// An associative list of /singleton/trait and trait level - See individual traits for valid levels
+ /// An associative list of /singleton/trait and trait level a species starts with by default - See individual traits for valid levels
var/list/traits = list()
/**
@@ -769,6 +769,22 @@ The slots that you can use are found in items_clothing.dm and are the inventory
return facial_hair_style_by_gender
+/datum/species/proc/get_selectable_traits()
+ var/list/allowed_traits = list()
+ var/list/trait_list = GET_SINGLETON_SUBTYPE_LIST(/singleton/trait)
+ for (var/singleton/trait/allowed_trait in trait_list)
+ if (!allowed_trait.selectable)
+ continue
+ if (LAZYISIN(traits, allowed_trait.type))
+ continue
+ if (LAZYISIN(allowed_trait.forbidden_species, name))
+ continue
+ if (!allowed_trait.name)
+ continue
+ LAZYSET(allowed_traits, allowed_trait.name, allowed_trait)
+
+ return allowed_traits
+
/datum/species/proc/get_description(header, append, verbose = TRUE, skip_detail, skip_photo)
var/list/damage_types = list(
"physical trauma" = brute_mod,
diff --git a/test/check-paths.sh b/test/check-paths.sh
index 5d18713d1f41b..9f3300ab701d6 100755
--- a/test/check-paths.sh
+++ b/test/check-paths.sh
@@ -44,7 +44,7 @@ exactly 0 "world.log<< uses" 'world.log<<|world.log[[:space:]]<<'
exactly 2 "<< uses" '(?> uses" '(?)>>(?!>)' -P
exactly 0 "incorrect indentations" '^( {4,})' -P
-exactly 25 "text2path uses" 'text2path'
+exactly 27 "text2path uses" 'text2path'
exactly 5 "update_icon() override" '/update_icon\((.*)\)' -P
exactly 4 "goto use" 'goto '
exactly 1 "NOOP match" 'NOOP'