From d2e5e35d6891b54df3ca7497c5d209e97150f41d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Apr 2026 13:03:26 -0500 Subject: [PATCH] Keen Nose Quirk - The Smell of Discovery (#95697) ## About The Pull Request This adds a new positive quirk called "**Keen Nose**" that costs 3 points. It lets you examine any open container with reagents to smell them. It generates the smell based on the taste description. Also added an effect to pepper spray, where it causes you to lose your sense of smell temporarily. This quirk is blacklisted against the Anosmia quirk, which is the opposite. (no sense of smell) ## Why It's Good For The Game Since there is a serious lack of positive quirks, I figured this one might be useful as a utility quirk. It's a niche thing that could come in handy. Keep in mind, there are several ways to identify chemicals, like using the bartender's beer goggles, the science goggles, a chem machine, or just eyeballing the color. Also, many chems have overlapping taste descriptions (several different things taste like "death") so it's not foolproof. --- code/__DEFINES/traits/declarations.dm | 1 + code/_globalvars/traits/_traits.dm | 1 + code/_globalvars/traits/admin_tooling.dm | 1 + .../subsystem/processing/quirks.dm | 1 + .../quirks/positive_quirks/keen_nose.dm | 9 +++ code/game/atom/atom_examine.dm | 22 +++++- .../chemistry/reagents/food_reagents.dm | 2 + code/modules/reagents/chemistry/taste.dm | 77 ++++++++++--------- tgstation.dme | 1 + 9 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 code/datums/quirks/positive_quirks/keen_nose.dm diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index aa4e2cf9d06..e3c4bdd8f77 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1041,6 +1041,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_SETTLER "settler" #define TRAIT_STRONG_STOMACH "strong_stomach" #define TRAIT_VEGETARIAN "trait_vegetarian" +#define TRAIT_KEEN_NOSE "keen_nose" /// This mob always lands on their feet when they fall, for better or for worse. #define TRAIT_CATLIKE_GRACE "catlike_grace" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 813d7d13e9e..92010822738 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -365,6 +365,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_IN_CALL" = TRAIT_IN_CALL, "TRAIT_IS_WET" = TRAIT_IS_WET, "TRAIT_IWASBATONED" = TRAIT_IWASBATONED, + "TRAIT_KEEN_NOSE" = TRAIT_KEEN_NOSE, "TRAIT_KISS_OF_DEATH" = TRAIT_KISS_OF_DEATH, "TRAIT_SEE_BLESSED_TILES" = TRAIT_SEE_BLESSED_TILES, "TRAIT_SYNDIE_KISS" = TRAIT_SYNDIE_KISS, diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm index 0b1e2f2ba84..3e9622b79d0 100644 --- a/code/_globalvars/traits/admin_tooling.dm +++ b/code/_globalvars/traits/admin_tooling.dm @@ -160,6 +160,7 @@ GLOBAL_LIST_INIT(admin_visible_traits, list( "TRAIT_INTROVERT" = TRAIT_INTROVERT, "TRAIT_INVISIBLE_MAN" = TRAIT_INVISIBLE_MAN, "TRAIT_IWASBATONED" = TRAIT_IWASBATONED, + "TRAIT_KEEN_NOSE" = TRAIT_KEEN_NOSE, "TRAIT_KISS_OF_DEATH" = TRAIT_KISS_OF_DEATH, "TRAIT_KNOCKEDOUT" = TRAIT_KNOCKEDOUT, "TRAIT_KNOW_ENGI_WIRES" = TRAIT_KNOW_ENGI_WIRES, diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 5c4239962b2..9bc33a49573 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -26,6 +26,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list( list(/datum/quirk/settler, /datum/quirk/freerunning), list(/datum/quirk/numb, /datum/quirk/selfaware), list(/datum/quirk/empath, /datum/quirk/evil), + list(/datum/quirk/keen_nose, /datum/quirk/item_quirk/anosmia), )) GLOBAL_LIST_INIT(quirk_string_blacklist, generate_quirk_string_blacklist()) diff --git a/code/datums/quirks/positive_quirks/keen_nose.dm b/code/datums/quirks/positive_quirks/keen_nose.dm new file mode 100644 index 00000000000..2681d34b86d --- /dev/null +++ b/code/datums/quirks/positive_quirks/keen_nose.dm @@ -0,0 +1,9 @@ +/datum/quirk/keen_nose + name = "Keen Nose" + desc = "You have a very sensitive nose. You can smell the contents of any open containers you are holding simply by examining them." + icon = FA_ICON_FLASK + value = 3 + mob_trait = TRAIT_KEEN_NOSE + gain_text = span_notice("You can smell everything around you much more clearly.") + lose_text = span_danger("Your sense of smell returns to normal.") + medical_record_text = "Patient has an unusually sensitive olfactory system." diff --git a/code/game/atom/atom_examine.dm b/code/game/atom/atom_examine.dm index 02e66ab749e..d2dc54d5d50 100644 --- a/code/game/atom/atom_examine.dm +++ b/code/game/atom/atom_examine.dm @@ -44,7 +44,6 @@ if(reagents.is_reacting) . += span_warning("It is currently reacting!") . += span_notice("The solution's pH is [round(reagents.ph, 0.01)] and has a temperature of [reagents.chem_temp]K.") - else . += "It contains:
Nothing." else if(reagents.flags & AMOUNT_VISIBLE) @@ -53,8 +52,29 @@ else . += span_danger("It's empty.") + if(HAS_TRAIT(user, TRAIT_KEEN_NOSE)) + var/sniff_text = get_sniff_examine(user) + if(sniff_text) + . += sniff_text + SEND_SIGNAL(src, COMSIG_ATOM_EXAMINE, user, .) +/// Returns an examine string describing what the contents of this atom smell like +/atom/proc/get_sniff_examine(mob/living/carbon/sniffer) + if(!istype(sniffer) || HAS_TRAIT(sniffer, TRAIT_ANOSMIA)) + return + if(!is_open_container() || !reagents?.total_volume) + return + if(!sniffer.is_holding(src)) + return + if(!sniffer.get_bodypart(BODY_ZONE_HEAD)) // Need a nose to smell + return + if(sniffer.is_mouth_covered()) + return span_warning("You can't get a whiff of [src] with your face covered.") + + var/smell_message = generate_reagents_taste_message(reagents.reagent_list, sniffer, 10) + return span_notice("You catch a whiff of [src]. It smells like [smell_message].") + /** * A list of "tags" displayed after atom's description in examine. * This should return an assoc list of tags -> tooltips for them. If item is null, then no tooltip is assigned. diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm index 38be6d99f23..b10f040c8f0 100644 --- a/code/modules/reagents/chemistry/reagents/food_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm @@ -473,6 +473,8 @@ victim.Knockdown(3 SECONDS) victim.add_movespeed_modifier(/datum/movespeed_modifier/reagent/pepperspray) addtimer(CALLBACK(victim, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/reagent/pepperspray), 10 SECONDS) + ADD_TRAIT(victim, TRAIT_ANOSMIA, type) + addtimer(TRAIT_CALLBACK_REMOVE(victim, TRAIT_ANOSMIA, type), 30 SECONDS, TIMER_OVERRIDE|TIMER_UNIQUE) victim.update_damage_hud() if(methods & INGEST) if(!holder.has_reagent(/datum/reagent/consumable/milk)) diff --git a/code/modules/reagents/chemistry/taste.dm b/code/modules/reagents/chemistry/taste.dm index 06cee0bba2d..d81a0ef1851 100644 --- a/code/modules/reagents/chemistry/taste.dm +++ b/code/modules/reagents/chemistry/taste.dm @@ -1,41 +1,45 @@ #define TEXT_NO_TASTE "something indescribable" -//================================TASTE=================================================== +//=============================== TASTE GRAPH ==================================== +// // +// Flavor Intensity Thresholds (Relative to Detection Threshold % 'DT'): // +// 0% 1x DT 2x DT 4x DT 100% // +// |----------------|------------------|------------------|-----------------| // +// | Undetected | Weak | Mild | Strong | // +// |----------------|------------------|------------------|-----------------| // +// // +//==============================================================================// + /** - * Returns what this reagents in our given list taste like + * Returns what the reagents in our given list taste like * * Arguments: * * list/reagent_list - List of reagents to taste. * * mob/living/taster - Who is doing the tasting. Some mobs can pick up specific flavours. - * * minimum_percent - The lower the minimum percent, the more sensitive the message is. - * * weight_modifier - Value to multiply each reagent's taste weight with. + * * detection_threshold_percent - The minimum relative percentage a flavor must reach to be tasted. */ -/proc/generate_reagents_taste_message(list/reagent_list, mob/living/taster, minimum_percent) +/proc/generate_reagents_taste_message(list/reagent_list, mob/living/taster, detection_threshold_percent) // We can't taste anything - if(minimum_percent > 100) + if(detection_threshold_percent > 100) return TEXT_NO_TASTE - // Associative list of our tastes, descriptor - strength + // Associative list of our tastes - list("taste description" = strength) var/list/tastes = list() - // Total of our taste strengths so far - var/total_taste = 0 + var/total_taste_strength = 0 for(var/datum/reagent/reagent as anything in reagent_list) if(!reagent.taste_mult) continue var/list/taste_data = reagent.get_taste_description(taster) - for(var/taste in taste_data) - var/taste_strength = taste_data[taste] * reagent.volume * reagent.taste_mult - if(taste in tastes) - tastes[taste] += taste_strength - else - tastes[taste] = taste_strength - total_taste += taste_strength + for(var/taste_desc in taste_data) + var/taste_strength = taste_data[taste_desc] * reagent.volume * reagent.taste_mult + tastes[taste_desc] += taste_strength + total_taste_strength += taste_strength // None of our reagents had any flavour - if(total_taste <= 0) + if(total_taste_strength <= 0) return TEXT_NO_TASTE // If we have exactly one taste, don't bother with relative strengths @@ -46,32 +50,33 @@ sortTim(tastes, cmp = GLOBAL_PROC_REF(cmp_numeric_dsc), associative = TRUE) // Lazylists for different taste strength categories, no need to initialize if we don't have such flavors - var/list/strong - var/list/mild - var/list/hint + var/list/strong_tastes + var/list/mild_tastes + var/list/weak_tastes for(var/taste_desc in tastes) - var/percent = tastes[taste_desc]/total_taste * 100 - if(percent < minimum_percent) - continue + var/relative_taste_percent = (tastes[taste_desc] / total_taste_strength) * 100 - if(percent <= minimum_percent * 2) - LAZYADD(hint, taste_desc) - else if(percent > minimum_percent * 4) - LAZYADD(strong, taste_desc) + // From weakest to strongest + if(relative_taste_percent < detection_threshold_percent) + continue // too weak to detect + else if(relative_taste_percent <= detection_threshold_percent * 2) + LAZYADD(weak_tastes, taste_desc) + else if(relative_taste_percent <= detection_threshold_percent * 4) + LAZYADD(mild_tastes, taste_desc) else - LAZYADD(mild, taste_desc) + LAZYADD(strong_tastes, taste_desc) - var/list/out = list() + var/list/taste_messages = list() - if(LAZYLEN(strong)) - out += "the strong flavor of [english_list(strong, TEXT_NO_TASTE)]" - if(LAZYLEN(mild)) + if(LAZYLEN(strong_tastes)) + taste_messages += "the strong flavor of [english_list(strong_tastes, TEXT_NO_TASTE)]" + if(LAZYLEN(mild_tastes)) // Prefix "some " if there are strong flavors to avoid seeming like a strong flavor - out += "[LAZYLEN(strong) ? "some " : ""][english_list(mild, TEXT_NO_TASTE)]" - if(LAZYLEN(hint)) - out += "a hint of [english_list(hint, TEXT_NO_TASTE)]" + taste_messages += "[LAZYLEN(strong_tastes) ? "some " : ""][english_list(mild_tastes, TEXT_NO_TASTE)]" + if(LAZYLEN(weak_tastes)) + taste_messages += "a hint of [english_list(weak_tastes, TEXT_NO_TASTE)]" - return english_list(out, TEXT_NO_TASTE) + return english_list(taste_messages, TEXT_NO_TASTE) #undef TEXT_NO_TASTE diff --git a/tgstation.dme b/tgstation.dme index 717fc41a448..8d6ff832949 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1981,6 +1981,7 @@ #include "code\datums\quirks\positive_quirks\freerunning.dm" #include "code\datums\quirks\positive_quirks\friendly.dm" #include "code\datums\quirks\positive_quirks\jolly.dm" +#include "code\datums\quirks\positive_quirks\keen_nose.dm" #include "code\datums\quirks\positive_quirks\light_step.dm" #include "code\datums\quirks\positive_quirks\mime_fan.dm" #include "code\datums\quirks\positive_quirks\musician.dm"