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.
This commit is contained in:
Tim
2026-04-13 13:03:26 -05:00
committed by GitHub
parent fb7e230315
commit d2e5e35d68
9 changed files with 78 additions and 37 deletions
+1
View File
@@ -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"
+1
View File
@@ -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,
+1
View File
@@ -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,
@@ -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())
@@ -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."
+21 -1
View File
@@ -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:<br>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.
@@ -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))
+41 -36
View File
@@ -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
+1
View File
@@ -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"