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'