From 117c2edc20c202a5a95cec48aeafebbb1020ea6f Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sat, 21 Oct 2023 21:14:19 +0200 Subject: [PATCH] [MIRROR] Adds Food Allergies as a counterpart to Medicine Allergies [MDB IGNORE] (#24493) * Adds Food Allergies as a counterpart to Medicine Allergies (#79118) ## About The Pull Request Adds food allergies as a counterpart to medicinal allergies. This is about as you would expect. Eating food you are allergic to causes Anaphylaxis, which can be cured with epinephrine. If you are not cured rapidly, you will shortly fall into a coma and die. There is a preference for choosing your character's allergy. Not all food types are viable allergen types: Only `"Alcohol", "Bugs", "Dairy", "Fruit", "Grain", "Meat", "Nuts", "Seafood", "Sugar", "Vegetables"`. You also get a dogtag like med allergies. Minor implementation detail: I added a "dangerous" level of food quality / scale, so I gave it to the Death Sandwich. ## Why It's Good For The Game This was a request, and I saw it fairly harmless, so I said why not. Similar to Medicine Allergy, it adds a small mechanic to concern yourself with when you visit the kitchen, encouraging you to get a bit more familiar with food types before diving into food at the kitchen. As it is not as impactful as Medicine Allergy, it is only a -2 quirk. ## Changelog :cl: Melbert add: Adds Food Allergies as a -2 quirk. You can select which food you're allergic to or rock a random option. /:cl: * Adds Food Allergies as a counterpart to Medicine Allergies --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> --- code/__DEFINES/food.dm | 3 + code/datums/components/food/edible.dm | 65 ++++++++++----- code/datums/diseases/anaphylaxis.dm | 83 +++++++++++++++++++ code/datums/mood_events/food_events.dm | 5 ++ .../quirks/negative_quirks/food_allergy.dm | 45 ++++++++++ code/game/objects/items/food/sandwichtoast.dm | 2 +- .../client/preferences/food_allergy.dm | 20 +++++ code/modules/mob/living/taste.dm | 8 ++ tgstation.dme | 3 + .../character_preferences/food_allergy.tsx | 6 ++ 10 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 code/datums/diseases/anaphylaxis.dm create mode 100644 code/datums/quirks/negative_quirks/food_allergy.dm create mode 100644 code/modules/client/preferences/food_allergy.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/food_allergy.tsx diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm index aa7833711eb..85ced17e67c 100644 --- a/code/__DEFINES/food.dm +++ b/code/__DEFINES/food.dm @@ -169,6 +169,8 @@ GLOBAL_LIST_INIT(food_buffs, list( #define LIKED_FOOD_QUALITY_CHANGE 2 /// Threshold for food to give a toxic reaction #define TOXIC_FOOD_QUALITY_THRESHOLD -8 +/// Food is dangerous to consume +#define FOOD_QUALITY_DANGEROUS -100 /// Food is "in a container", not in a code sense, but in a literal sense (canned foods) #define FOOD_IN_CONTAINER (1<<0) @@ -200,6 +202,7 @@ DEFINE_BITFIELD(food_flags, list( #define FOOD_LIKED 1 #define FOOD_DISLIKED 2 #define FOOD_TOXIC 3 +#define FOOD_ALLERGIC 4 ///Venue reagent requirement #define VENUE_BAR_MINIMUM_REAGENTS 10 diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm index 7c86ddc2243..dd9c91c6ec5 100644 --- a/code/datums/components/food/edible.dm +++ b/code/datums/components/food/edible.dm @@ -223,6 +223,8 @@ Behavior that's still missing from this component that original food items had t examine_list += span_green("You find this meal [quality_label].") else if (quality == 0) examine_list += span_notice("You find this meal edible.") + else if (quality <= FOOD_QUALITY_DANGEROUS) + examine_list += span_warning("You may die from eating this meal.") else if (quality <= TOXIC_FOOD_QUALITY_THRESHOLD) examine_list += span_warning("You find this meal disgusting!") else @@ -526,35 +528,48 @@ Behavior that's still missing from this component that original food items had t if(!ishuman(eater)) return FALSE var/mob/living/carbon/human/gourmand = eater - //Bruh this breakfast thing is cringe and shouldve been handled separately from food-types, remove this in the future (Actually, just kill foodtypes in general) - if((foodtypes & BREAKFAST) && world.time - SSticker.round_start_time < STOP_SERVING_BREAKFAST) - gourmand.add_mood_event("breakfast", /datum/mood_event/breakfast) - last_check_time = world.time - - var/food_quality = get_perceived_food_quality(gourmand, parent) - if(food_quality <= TOXIC_FOOD_QUALITY_THRESHOLD) - to_chat(gourmand,span_warning("What the hell was that thing?!")) - gourmand.adjust_disgust(25 + 30 * fraction) - gourmand.add_mood_event("toxic_food", /datum/mood_event/disgusting_food) - else if(food_quality < 0) - to_chat(gourmand,span_notice("That didn't taste very good...")) - gourmand.adjust_disgust(11 + 15 * fraction) - gourmand.add_mood_event("gross_food", /datum/mood_event/gross_food) - else if(food_quality > 0) - food_quality = min(food_quality, FOOD_QUALITY_TOP) - var/atom/owner = parent - var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // mood event duration is 100% at average purity of 50% - var/event = GLOB.food_quality_events[food_quality] - gourmand.add_mood_event("quality_food", event, timeout_mod) - gourmand.adjust_disgust(-5 + -2 * food_quality * fraction) - var/quality_label = GLOB.food_quality_description[food_quality] - to_chat(gourmand, span_notice("That's \an [quality_label] meal.")) if(istype(parent, /obj/item/food)) var/obj/item/food/food = parent if(food.venue_value >= FOOD_PRICE_EXOTIC) gourmand.add_mob_memory(/datum/memory/good_food, food = parent) + //Bruh this breakfast thing is cringe and shouldve been handled separately from food-types, remove this in the future (Actually, just kill foodtypes in general) + if((foodtypes & BREAKFAST) && world.time - SSticker.round_start_time < STOP_SERVING_BREAKFAST) + gourmand.add_mood_event("breakfast", /datum/mood_event/breakfast) + last_check_time = world.time + + var/food_quality = get_perceived_food_quality(gourmand) + if(food_quality <= FOOD_QUALITY_DANGEROUS && (foodtypes & gourmand.get_allergic_foodtypes())) // Only cause anaphylaxis if we're ACTUALLY allergic, otherwise it just tastes horrible + if(gourmand.ForceContractDisease(new /datum/disease/anaphylaxis(), make_copy = FALSE, del_on_fail = TRUE)) + to_chat(gourmand, span_warning("You feel your throat start to itch.")) + gourmand.add_mood_event("allergic_food", /datum/mood_event/allergic_food) + return + + if(food_quality <= TOXIC_FOOD_QUALITY_THRESHOLD) + to_chat(gourmand,span_warning("What the hell was that thing?!")) + gourmand.adjust_disgust(25 + 30 * fraction) + gourmand.add_mood_event("toxic_food", /datum/mood_event/disgusting_food) + return + + if(food_quality < 0) + to_chat(gourmand,span_notice("That didn't taste very good...")) + gourmand.adjust_disgust(11 + 15 * fraction) + gourmand.add_mood_event("gross_food", /datum/mood_event/gross_food) + return + + if(food_quality == 0) + return // meh + + food_quality = min(food_quality, FOOD_QUALITY_TOP) + var/atom/owner = parent + var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // mood event duration is 100% at average purity of 50% + var/event = GLOB.food_quality_events[food_quality] + gourmand.add_mood_event("quality_food", event, timeout_mod) + gourmand.adjust_disgust(-5 + -2 * food_quality * fraction) + var/quality_label = GLOB.food_quality_description[food_quality] + to_chat(gourmand, span_notice("That's \an [quality_label] meal.")) + /// Get the complexity of the crafted food /datum/component/edible/proc/get_recipe_complexity() if(!HAS_TRAIT(parent, TRAIT_FOOD_CHEF_MADE) || !istype(parent, /obj/item/food)) @@ -580,8 +595,12 @@ Behavior that's still missing from this component that original food items had t return DISLIKED_FOOD_QUALITY_CHANGE if(FOOD_TOXIC) return TOXIC_FOOD_QUALITY_THRESHOLD + if(FOOD_ALLERGIC) + return FOOD_QUALITY_DANGEROUS if(ishuman(eater)) + if(foodtypes & eater.get_allergic_foodtypes()) + return FOOD_QUALITY_DANGEROUS if(count_matching_foodtypes(foodtypes, eater.get_toxic_foodtypes())) //if the food is toxic, we don't care about anything else return TOXIC_FOOD_QUALITY_THRESHOLD if(HAS_TRAIT(eater, TRAIT_AGEUSIA)) //if you can't taste it, it doesn't taste good diff --git a/code/datums/diseases/anaphylaxis.dm b/code/datums/diseases/anaphylaxis.dm new file mode 100644 index 00000000000..12d408ad215 --- /dev/null +++ b/code/datums/diseases/anaphylaxis.dm @@ -0,0 +1,83 @@ +/datum/disease/anaphylaxis + form = "Shock" + name = "Anaphylaxis" + desc = "Patient is undergoing a life-threatening allergic reaction and will die if not treated." + max_stages = 3 + cure_text = "Epinephrine" + cures = list(/datum/reagent/medicine/epinephrine) + cure_chance = 20 + agent = "Allergy" + viable_mobtypes = list(/mob/living/carbon/human) + disease_flags = CURABLE + severity = DISEASE_SEVERITY_DANGEROUS + spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS + spread_text = "None" + visibility_flags = HIDDEN_PANDEMIC + bypasses_immunity = TRUE + stage_prob = 5 + +/datum/disease/anaphylaxis/stage_act(seconds_per_tick, times_fired) + . = ..() + if(!.) + return + + if(HAS_TRAIT(affected_mob, TRAIT_TOXINLOVER)) // You are no fun + cure() + return + + // Cool them enough to feel cold to the touch, and then some, because temperature mechanics are dumb + affected_mob.adjust_bodytemperature(-10 * seconds_per_tick * stage, min_temp = BODYTEMP_COLD_DAMAGE_LIMIT - 70) + + switch(stage) + // early symptoms: mild shakes and dizziness + if(1) + if(affected_mob.num_hands >= 1 && SPT_PROB(5, seconds_per_tick)) + to_chat(affected_mob, span_warning("You feel your hand[affected_mob.num_hands == 1 ? "":"s"] start to shake.")) + affected_mob.adjust_jitter_up_to(4 SECONDS * seconds_per_tick, 1 MINUTES) + if(affected_mob.num_legs >= 1 && SPT_PROB(5, seconds_per_tick)) + to_chat(affected_mob, span_warning("You feel your leg[affected_mob.num_hands == 1 ? "":"s"] start to shake.")) + affected_mob.adjust_jitter_up_to(4 SECONDS * seconds_per_tick, 1 MINUTES) + if(SPT_PROB(2, seconds_per_tick)) + affected_mob.adjust_dizzy_up_to(5 SECONDS * seconds_per_tick, 1 MINUTES) + if(SPT_PROB(1, seconds_per_tick)) + to_chat(affected_mob, span_danger("Your throat itches.")) + + // warning symptoms: violent shakes, dizziness, blurred vision, difficulty breathing + if(2) + affected_mob.apply_damage(0.33 * seconds_per_tick, TOX, spread_damage = TRUE) + + if(affected_mob.num_hands >= 1 && SPT_PROB(5, seconds_per_tick)) + to_chat(affected_mob, span_warning("You feel your hand[affected_mob.num_hands == 1 ? "":"s"] shake violently.")) + affected_mob.adjust_jitter_up_to(8 SECONDS * seconds_per_tick, 1 MINUTES) + if(prob(20)) + affected_mob.drop_all_held_items() + if(affected_mob.num_legs >= 1 && SPT_PROB(5, seconds_per_tick)) + to_chat(affected_mob, span_warning("You feel your leg[affected_mob.num_hands == 1 ? "":"s"] shake violently.")) + affected_mob.adjust_jitter_up_to(8 SECONDS * seconds_per_tick, 1 MINUTES) + if(prob(40) && affected_mob.getStaminaLoss() < 75) + affected_mob.adjustStaminaLoss(15) + if(affected_mob.get_organ_slot(ORGAN_SLOT_EYES) && SPT_PROB(4, seconds_per_tick)) + affected_mob.adjust_eye_blur(4 SECONDS * seconds_per_tick) + to_chat(affected_mob, span_warning("It's getting harder to see clearly.")) + if(!HAS_TRAIT(affected_mob, TRAIT_NOBREATH) && SPT_PROB(4, seconds_per_tick)) + affected_mob.apply_damage(2 * seconds_per_tick, OXY) + affected_mob.losebreath += (2 * seconds_per_tick) + to_chat(affected_mob, span_warning("It's getting harder to breathe.")) + if(SPT_PROB(2, seconds_per_tick)) + affected_mob.adjust_drowsiness_up_to(3 SECONDS * seconds_per_tick, 30 SECONDS) + if(SPT_PROB(2, seconds_per_tick)) + affected_mob.adjust_dizzy_up_to(5 SECONDS * seconds_per_tick, 1 MINUTES) + affected_mob.adjust_confusion_up_to(1 SECONDS * seconds_per_tick, 10 SECONDS) + if(SPT_PROB(2, seconds_per_tick)) + affected_mob.vomit(MOB_VOMIT_MESSAGE|MOB_VOMIT_HARM) + affected_mob.Stun(2 SECONDS) // The full 20 second vomit stun would be lethal + if(SPT_PROB(1, seconds_per_tick)) + affected_mob.emote("cough") + if(SPT_PROB(1, seconds_per_tick)) + to_chat(affected_mob, span_danger("Your throat feels sore.")) + + // "you are too late" symptoms: death. + if(3) + affected_mob.apply_damage(3 * seconds_per_tick, TOX, spread_damage = TRUE) + affected_mob.apply_damage(1 * seconds_per_tick, OXY) + affected_mob.Unconscious(3 SECONDS * seconds_per_tick) diff --git a/code/datums/mood_events/food_events.dm b/code/datums/mood_events/food_events.dm index 7d2dcc439de..7d33e7e57ce 100644 --- a/code/datums/mood_events/food_events.dm +++ b/code/datums/mood_events/food_events.dm @@ -13,6 +13,11 @@ mood_change = -6 timeout = 4 MINUTES +/datum/mood_event/allergic_food + description = "My throat itches." + mood_change = -2 + timeout = 4 MINUTES + /datum/mood_event/breakfast description = "Nothing like a hearty breakfast to start the shift." mood_change = 2 diff --git a/code/datums/quirks/negative_quirks/food_allergy.dm b/code/datums/quirks/negative_quirks/food_allergy.dm new file mode 100644 index 00000000000..c2f4eae4d0e --- /dev/null +++ b/code/datums/quirks/negative_quirks/food_allergy.dm @@ -0,0 +1,45 @@ +GLOBAL_LIST_INIT(possible_food_allergies, list( + "Alcohol" = ALCOHOL, + "Bugs" = BUGS, + "Dairy" = DAIRY, + "Fruit" = FRUIT, + "Grain" = GRAIN, + "Meat" = MEAT, + "Nuts" = NUTS, + "Seafood" = SEAFOOD, + "Sugar" = SUGAR, + "Vegetables" = VEGETABLES, +)) + +/datum/quirk/item_quirk/food_allergic + name = "Food Allergy" + desc = "Ever since you were a kid, you've been allergic to certain foods." + icon = FA_ICON_SHRIMP + value = -2 + gain_text = span_danger("You feel your immune system shift.") + lose_text = span_notice("You feel your immune system phase back into perfect shape.") + medical_record_text = "Patient's immune system responds violently to certain food." + hardcore_value = 1 + quirk_flags = QUIRK_HUMAN_ONLY + mail_goodies = list(/obj/item/reagent_containers/hypospray/medipen) + /// Footype flags that will trigger the allergy + var/target_foodtypes = NONE + +/datum/quirk/item_quirk/food_allergic/add(client/client_source) + if(target_foodtypes != NONE) // Already set, don't care + return + + var/desired_allergy = client_source?.prefs.read_preference(/datum/preference/choiced/food_allergy) || "Random" + if(desired_allergy != "Random") + target_foodtypes = GLOB.possible_food_allergies[desired_allergy] + if(target_foodtypes != NONE) // Got a preference, don't care + return + + target_foodtypes = pick(flatten_list(GLOB.possible_food_allergies)) + +/datum/quirk/item_quirk/food_allergic/add_unique(client/client_source) + var/what_are_we_actually_killed_by = english_list(bitfield_to_list(target_foodtypes, FOOD_FLAGS_IC)) // This should never be more than one thing but just in case we can support it + to_chat(client_source.mob, span_info("You are allergic to [what_are_we_actually_killed_by]. Watch what you eat!")) + + var/obj/item/clothing/accessory/dogtag/allergy/dogtag = new(quirk_holder, what_are_we_actually_killed_by) + give_item_to_holder(dogtag, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS), flavour_text = "Keep it close around the kitchen.") diff --git a/code/game/objects/items/food/sandwichtoast.dm b/code/game/objects/items/food/sandwichtoast.dm index 8b91b04621e..c6488f67a1e 100644 --- a/code/game/objects/items/food/sandwichtoast.dm +++ b/code/game/objects/items/food/sandwichtoast.dm @@ -284,7 +284,7 @@ // Closest thing to a mullet we have if(consumer.hairstyle == "Gelled Back" && istype(consumer.get_item_by_slot(ITEM_SLOT_ICLOTHING), /obj/item/clothing/under/rank/civilian/cookjorts)) return FOOD_LIKED - return FOOD_DISLIKED + return FOOD_ALLERGIC /** * Callback to be used with the edible component. diff --git a/code/modules/client/preferences/food_allergy.dm b/code/modules/client/preferences/food_allergy.dm new file mode 100644 index 00000000000..461c3b31e2a --- /dev/null +++ b/code/modules/client/preferences/food_allergy.dm @@ -0,0 +1,20 @@ +/datum/preference/choiced/food_allergy + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + savefile_key = "food_allergy" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + +/datum/preference/choiced/food_allergy/init_possible_values() + return list("Random") + assoc_to_keys(GLOB.possible_food_allergies) + +/datum/preference/choiced/food_allergy/create_default_value() + return "Random" + +/datum/preference/choiced/food_allergy/is_accessible(datum/preferences/preferences) + if (!..()) + return FALSE + + return "Food Allergy" in preferences.all_quirks + +/datum/preference/choiced/food_allergy/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/code/modules/mob/living/taste.dm b/code/modules/mob/living/taste.dm index 8f414a2e603..f0bc01eb84c 100644 --- a/code/modules/mob/living/taste.dm +++ b/code/modules/mob/living/taste.dm @@ -85,6 +85,14 @@ return TOXIC return tongue.toxic_foodtypes +/** + * Gets food this mob is allergic to + * Essentially toxic food+, not only disgusting but outright lethal + */ +/mob/living/proc/get_allergic_foodtypes() + var/datum/quirk/item_quirk/food_allergic/allergy = get_quirk(/datum/quirk/item_quirk/food_allergic) + return allergy?.target_foodtypes || NONE + /** * Gets the food reaction a mob would normally have from the given food item, * assuming that no check_liked callback was used in the edible component. diff --git a/tgstation.dme b/tgstation.dme index bcee1bb3d2c..d79b29114ef 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1294,6 +1294,7 @@ #include "code\datums\diseases\_disease.dm" #include "code\datums\diseases\_MobProcs.dm" #include "code\datums\diseases\adrenal_crisis.dm" +#include "code\datums\diseases\anaphylaxis.dm" #include "code\datums\diseases\anxiety.dm" #include "code\datums\diseases\beesease.dm" #include "code\datums\diseases\brainrot.dm" @@ -1649,6 +1650,7 @@ #include "code\datums\quirks\negative_quirks\deafness.dm" #include "code\datums\quirks\negative_quirks\depression.dm" #include "code\datums\quirks\negative_quirks\family_heirloom.dm" +#include "code\datums\quirks\negative_quirks\food_allergy.dm" #include "code\datums\quirks\negative_quirks\frail.dm" #include "code\datums\quirks\negative_quirks\glass_jaw.dm" #include "code\datums\quirks\negative_quirks\heavy_sleeper.dm" @@ -3538,6 +3540,7 @@ #include "code\modules\client\preferences\broadcast_login_logout.dm" #include "code\modules\client\preferences\clothing.dm" #include "code\modules\client\preferences\darkened_flash.dm" +#include "code\modules\client\preferences\food_allergy.dm" #include "code\modules\client\preferences\fov_darkness.dm" #include "code\modules\client\preferences\fps.dm" #include "code\modules\client\preferences\gender.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/food_allergy.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/food_allergy.tsx new file mode 100644 index 00000000000..f5b0ea37ca8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/food_allergy.tsx @@ -0,0 +1,6 @@ +import { FeatureChoiced, FeatureDropdownInput } from '../base'; + +export const food_allergy: FeatureChoiced = { + name: 'Food Allergy', + component: FeatureDropdownInput, +};