From 4cf04e9dad1bce8b9e7e1696522b8944a554c6de Mon Sep 17 00:00:00 2001 From: Chubbygummibear <46236974+Chubbygummibear@users.noreply.github.com> Date: Sat, 4 Jun 2022 08:05:57 -0700 Subject: [PATCH] Ports TG's eye contact: social anxiety harder, get caught staring at your obsession target, and new quirk Shifty Eyes (#14258) * eyeeyeseyes * updated --- code/__DEFINES/components.dm | 2 + code/__DEFINES/mobs.dm | 19 +++- code/__DEFINES/traits.dm | 2 + code/datums/brain_damage/creepy_trauma.dm | 51 ++++++----- code/datums/traits/negative.dm | 105 +++++++++++++++++++--- code/datums/traits/neutral.dm | 7 ++ code/game/atoms.dm | 4 +- code/modules/client/client_defines.dm | 2 +- code/modules/mob/living/carbon/carbon.dm | 3 + code/modules/mob/living/living.dm | 3 + code/modules/mob/mob.dm | 39 +++++++- 11 files changed, 196 insertions(+), 41 deletions(-) diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 9d7eb469cad7..9d0ca1711f5f 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -199,6 +199,8 @@ #define COMSIG_MOB_THROW "mob_throw" //from base of /mob/throw_item(): (atom/target) #define COMSIG_MOB_TABLING "mob_tabling" //form base of /obj/structure/table_place() and table_push(): (mob/living/user, mob/living/pushed_mob) #define COMSIG_MOB_EXAMINATE "mob_examinate" //from base of /mob/verb/examinate(): (atom/target) +#define COMSIG_MOB_EYECONTACT "mob_eyecontact" ///from /mob/living/handle_eye_contact(): (mob/living/other_mob) +#define COMSIG_BLOCK_EYECONTACT (1<<0) /// return this if you want to block printing this message to this person, if you want to print your own (does not affect the other person's message) #define COMSIG_MOB_UPDATE_SIGHT "mob_update_sight" //from base of /mob/update_sight(): () #define COMSIG_MOB_SAY "mob_say" // from /mob/living/say(): () #define COMPONENT_UPPERCASE_SPEECH 1 diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 99e9cb702f0c..a1b4601423a3 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -338,9 +338,24 @@ #define WABBAJACK (1<<6) #define SLEEP_CHECK_DEATH(X) sleep(X); if(QDELETED(src) || stat == DEAD) return; + +// recent examine defines +/// How long it takes for an examined atom to be removed from recent_examines. Should be the max of the below time windows +#define RECENT_EXAMINE_MAX_WINDOW 2 SECONDS /// If you examine the same atom twice in this timeframe, we call examine_more() instead of examine() -#define EXAMINE_MORE_TIME 1 SECONDS +#define EXAMINE_MORE_WINDOW 1 SECONDS +/// If you examine another mob who's successfully examined you during this duration of time, you two try to make eye contact. Cute! +#define EYE_CONTACT_WINDOW 2 SECONDS +/// If you yawn while someone nearby has examined you within this time frame, it will force them to yawn as well. Tradecraft! +#define YAWN_PROPAGATION_EXAMINE_WINDOW 2 SECONDS + +/// How far away you can be to make eye contact with someone while examining +#define EYE_CONTACT_RANGE 5 + +//this should be in the ai defines, but out ai defines are actual ai, not simplemob ai +#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat) + #define INTERACTING_WITH(X, Y) (Y in X.do_afters) -#define DOING_INTERACTION(user, interaction_key) (LAZYACCESS(user.do_afters, interaction_key)) \ No newline at end of file +#define DOING_INTERACTION(user, interaction_key) (LAZYACCESS(user.do_afters, interaction_key)) diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 363f9e4cef2f..c997234554e5 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -257,6 +257,8 @@ #define TRAIT_EAT_LESS "eat_less" #define TRAIT_CRAFTY "crafty" #define TRAIT_ANOREXIC "anorexic" +#define TRAIT_SHIFTY_EYES "shifty_eyes" +#define TRAIT_ANXIOUS "anxious" // common trait sources #define TRAIT_GENERIC "generic" diff --git a/code/datums/brain_damage/creepy_trauma.dm b/code/datums/brain_damage/creepy_trauma.dm index 0c406e7bc4ff..2b43e370b6eb 100644 --- a/code/datums/brain_damage/creepy_trauma.dm +++ b/code/datums/brain_damage/creepy_trauma.dm @@ -31,6 +31,7 @@ owner.mind.add_antag_datum(/datum/antagonist/obsessed) antagonist = owner.mind.has_antag_datum(/datum/antagonist/obsessed) antagonist.trauma = src + RegisterSignal(obsession, COMSIG_MOB_EYECONTACT, .proc/stare) ..() //antag stuff// antagonist.forge_objectives(obsession.mind) @@ -69,41 +70,47 @@ /datum/brain_trauma/special/obsessed/on_lose() ..() owner.mind.remove_antag_datum(/datum/antagonist/obsessed) + UnregisterSignal(obsession, COMSIG_MOB_EYECONTACT) /datum/brain_trauma/special/obsessed/handle_speech(datum/source, list/speech_args) if(!viewing) return - var/datum/component/mood/mood = owner.GetComponent(/datum/component/mood) - if(mood && mood.sanity >= SANITY_GREAT && social_interaction()) - speech_args[SPEECH_MESSAGE] = "" + if(prob(25)) // 25% chances to be nervous and stutter. + if(prob(50)) // 12.5% chance (previous check taken into account) of doing something suspicious. + addtimer(CALLBACK(src, .proc/on_failed_social_interaction), rand(1, 3) SECONDS) + else if(!owner.stuttering) + to_chat(owner, span_warning("Being near [obsession] makes you nervous and you begin to stutter...")) + owner.stuttering = max(3, owner.stuttering) /datum/brain_trauma/special/obsessed/on_hug(mob/living/hugger, mob/living/hugged) if(hugged == obsession) obsession_hug_count++ -/datum/brain_trauma/special/obsessed/proc/social_interaction() - var/fail = FALSE //whether you can finish a sentence while doing it - owner.stuttering = max(3, owner.stuttering) - owner.blur_eyes(10) - switch(rand(1,4)) - if(1) +/datum/brain_trauma/special/obsessed/proc/on_failed_social_interaction() + if(QDELETED(owner) || owner.stat >= UNCONSCIOUS) + return + switch(rand(1, 100)) + if(1 to 40) + INVOKE_ASYNC(owner, /mob.proc/emote, pick("blink", "blink_r")) + owner.blur_eyes(10) + to_chat(owner, span_userdanger("You sweat profusely and have a hard time focusing...")) + if(41 to 80) + INVOKE_ASYNC(owner, /mob.proc/emote, "pale") shake_camera(owner, 15, 1) - owner.vomit() - fail = TRUE - if(2) + owner.adjustStaminaLoss(70) + to_chat(owner, span_userdanger("You feel your heart lurching in your chest...")) + if(81 to 100) INVOKE_ASYNC(owner, /mob.proc/emote, "cough") owner.dizziness += 10 - fail = TRUE - if(3) - to_chat(owner, span_userdanger("You feel your heart lurching in your chest...")) - owner.Stun(20) - shake_camera(owner, 15, 1) - if(4) - to_chat(owner, span_warning("You faint.")) - owner.Unconscious(80) - fail = TRUE - return fail + owner.adjust_disgust(5) + to_chat(owner, span_userdanger("You gag and swallow a bit of bile...")) +/datum/brain_trauma/special/obsessed/proc/stare(datum/source, mob/living/examining_mob, triggering_examiner) + + if(examining_mob != owner || !triggering_examiner || prob(50)) + return + addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, obsession, span_warning("You catch [examining_mob] staring at you...")), 3) + return COMSIG_BLOCK_EYECONTACT /datum/brain_trauma/special/obsessed/proc/find_obsession() var/chosen_victim diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm index 2eb0c9105230..08398c1d5414 100644 --- a/code/datums/traits/negative.dm +++ b/code/datums/traits/negative.dm @@ -440,23 +440,104 @@ lose_text = span_notice("You feel easier about talking again.") //if only it were that easy! medical_record_text = "Patient is usually anxious in social encounters and prefers to avoid them." var/dumb_thing = TRUE + mob_trait = TRAIT_ANXIOUS -/datum/quirk/social_anxiety/on_process() +/datum/quirk/social_anxiety/add() + RegisterSignal(quirk_holder, COMSIG_MOB_EYECONTACT, .proc/eye_contact) + RegisterSignal(quirk_holder, COMSIG_MOB_EXAMINATE, .proc/looks_at_floor) + RegisterSignal(quirk_holder, COMSIG_MOB_SAY, .proc/handle_speech) + +/datum/quirk/social_anxiety/remove() + UnregisterSignal(quirk_holder, list(COMSIG_MOB_EYECONTACT, COMSIG_MOB_EXAMINATE, COMSIG_MOB_SAY)) + +/datum/quirk/social_anxiety/proc/handle_speech(datum/source, list/speech_args) + if(HAS_TRAIT(quirk_holder, TRAIT_FEARLESS)) + return + + var/datum/component/mood/mood = quirk_holder.GetComponent(/datum/component/mood) + var/moodmod + if(mood) + moodmod = (1+0.02*(50-(max(50, mood.mood_level*(7-mood.sanity_level))))) //low sanity levels are better, they max at 6 + else + moodmod = (1+0.02*(50-(max(50, 0.1*quirk_holder.nutrition)))) var/nearby_people = 0 for(var/mob/living/carbon/human/H in oview(3, quirk_holder)) if(H.client) nearby_people++ - var/mob/living/carbon/human/H = quirk_holder - if(prob(2 + nearby_people)) - H.stuttering = max(3, H.stuttering) - else if(prob(min(3, nearby_people)) && !H.silent) - to_chat(H, span_danger("You retreat into yourself. You really don't feel up to talking.")) - H.silent = max(10, H.silent) - else if(prob(0.5) && dumb_thing) - to_chat(H, span_userdanger("You think of a dumb thing you said a long time ago and scream internally.")) - dumb_thing = FALSE //only once per life - if(prob(1)) - new/obj/item/reagent_containers/food/snacks/spaghetti/pastatomato(get_turf(H)) //now that's what I call spaghetti code + var/message = speech_args[SPEECH_MESSAGE] + if(message) + var/list/message_split = splittext(message, " ") + var/list/new_message = list() + var/mob/living/carbon/human/quirker = quirk_holder + for(var/word in message_split) + if(prob(max(5,(nearby_people*12.5*moodmod))) && word != message_split[1]) //Minimum 1/20 chance of filler + new_message += pick("uh,","erm,","um,") + if(prob(min(5,(0.05*(nearby_people*12.5)*moodmod)))) //Max 1 in 20 chance of cutoff after a succesful filler roll, for 50% odds in a 15 word sentence + quirker.silent = max(3, quirker.silent) + to_chat(quirker, span_danger("You feel self-conscious and stop talking. You need a moment to recover!")) + break + if(prob(max(5,(nearby_people*12.5*moodmod)))) //Minimum 1/20 chance of stutter + // Add a short stutter, THEN treat our word + quirker.stuttering += max(3, quirker.stuttering) + new_message += quirker.treat_message(word) + + else + new_message += word + + message = jointext(new_message, " ") + var/mob/living/carbon/human/quirker = quirk_holder + if(prob(min(50,(0.50*(nearby_people*12.5)*moodmod)))) //Max 50% chance of not talking + if(dumb_thing) + to_chat(quirker, span_userdanger("You think of a dumb thing you said a long time ago and scream internally.")) + dumb_thing = FALSE //only once per life + if(prob(1)) + new/obj/item/reagent_containers/food/snacks/spaghetti/pastatomato(get_turf(quirker)) //now that's what I call spaghetti code + else + to_chat(quirk_holder, span_warning("You think that wouldn't add much to the conversation and decide not to say it.")) + if(prob(min(25,(0.25*(nearby_people*12.75)*moodmod)))) //Max 25% chance of silence stacks after succesful not talking roll + to_chat(quirker, span_danger("You retreat into yourself. You really don't feel up to talking.")) + quirker.silent = max(5, quirker.silent) + speech_args[SPEECH_MESSAGE] = pick("Uh.","Erm.","Um.") + else + speech_args[SPEECH_MESSAGE] = message + +// small chance to make eye contact with inanimate objects/mindless mobs because of nerves +/datum/quirk/social_anxiety/proc/looks_at_floor(datum/source, atom/A) + var/mob/living/mind_check = A + if(prob(85) || (istype(mind_check) && mind_check.mind)) + return + + addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, quirk_holder, span_smallnotice("You make eye contact with [A].")), 3) + +/datum/quirk/social_anxiety/proc/eye_contact(datum/source, mob/living/other_mob, triggering_examiner) + var/mob/living/carbon/human/quirker = quirk_holder + if(prob(75)) + return + var/msg + if(triggering_examiner) + msg = "You make eye contact with [other_mob], " + else + msg = "[other_mob] makes eye contact with you, " + + switch(rand(1,3)) + if(1) + quirker.Jitter(5) + msg += "causing you to start fidgeting!" + if(2) + quirker.stuttering = max(3, quirker.stuttering) + msg += "causing you to start stuttering!" + if(3) + quirker.Stun(2 SECONDS) + msg += "causing you to freeze up!" + + SEND_SIGNAL(quirk_holder, COMSIG_ADD_MOOD_EVENT, "anxiety_eyecontact", /datum/mood_event/anxiety_eyecontact) + addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, quirk_holder, span_userdanger("[msg]")), 3) // so the examine signal has time to fire and this will print after + return COMSIG_BLOCK_EYECONTACT + +/datum/mood_event/anxiety_eyecontact + description = "Sometimes eye contact makes me so nervous..." + mood_change = -5 + timeout = 3 MINUTES //If you want to make some kind of junkie variant, just extend this quirk. /datum/quirk/junkie diff --git a/code/datums/traits/neutral.dm b/code/datums/traits/neutral.dm index 804b370fce06..be584d0fd266 100644 --- a/code/datums/traits/neutral.dm +++ b/code/datums/traits/neutral.dm @@ -102,6 +102,13 @@ if(quirk_holder) quirk_holder.remove_client_colour(/datum/client_colour/monochrome) +/datum/quirk/shifty_eyes + name = "Shifty Eyes" + desc = "Your eyes tend to wander all over the place, whether you mean to or not, causing people to sometimes think you're looking directly at them when you aren't." + value = 0 + medical_record_text = "Fucking creep kept staring at me the whole damn checkup. I'm only diagnosing this because it's less awkward than thinking it was on purpose." + mob_trait = TRAIT_SHIFTY_EYES + /datum/quirk/random_accent name = "Randomized Accent" desc = "You have developed a random accent." diff --git a/code/game/atoms.dm b/code/game/atoms.dm index e823c68c0a8a..f5e43498dd2b 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -498,7 +498,7 @@ SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .) /** - * Called when a mob examines (shift click or verb) this atom twice (or more) within EXAMINE_MORE_TIME (default 1.5 seconds) + * Called when a mob examines (shift click or verb) this atom twice (or more) within EXAMINE_MORE_WINDOW (default 1.5 seconds) * * This is where you can put extra information on something that may be superfluous or not important in critical gameplay * moments, while allowing people to manually double-examine to take a closer look @@ -1380,4 +1380,4 @@ // first of all make sure we valid var/mouseparams = list2params(paramslist) usr_client.Click(src, loc, null, mouseparams) - return TRUE \ No newline at end of file + return TRUE diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 90fe42099249..c2e6a65b20c9 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -129,7 +129,7 @@ var/datum/viewData/view_size -///A lazy list of atoms we've examined in the last EXAMINE_MORE_TIME (default 1.5) seconds, so that we will call [atom/proc/examine_more()] instead of [atom/proc/examine()] on them when examining + ///A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [atom/proc/examine_more()] instead of [atom/proc/examine()] on them when examining var/list/recent_examines /// our current tab diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 9855b3ff36ec..8234f746b9f1 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -1236,6 +1236,9 @@ scaries.fake = TRUE QDEL_NULL(phantom_wound) +/mob/living/carbon/is_face_visible() + return !(wear_mask?.flags_inv & HIDEFACE) && !(head?.flags_inv & HIDEFACE) + /** * get_biological_state is a helper used to see what kind of wounds we roll for. By default we just assume carbons (read:monkeys) are flesh and bone, but humans rely on their species datums * diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index d516e88c4e8e..f431cabef095 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1456,3 +1456,6 @@ return TRUE return FALSE +/// Only defined for carbons who can wear masks and helmets, we just assume other mobs have visible faces +/mob/living/proc/is_face_visible() + return TRUE diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 9d08c7981040..4ce8a9c755ee 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -447,9 +447,10 @@ var/extra_info = A.examine_more(src) result = extra_info if(!result) - client.recent_examines[A] = world.time + EXAMINE_MORE_TIME + client.recent_examines[A] = world.time + EXAMINE_MORE_WINDOW result = A.examine(src) - addtimer(CALLBACK(src, .proc/clear_from_recent_examines, A), EXAMINE_MORE_TIME) + addtimer(CALLBACK(src, .proc/clear_from_recent_examines, A), RECENT_EXAMINE_MAX_WINDOW) + handle_eye_contact(A) else result = A.examine(src) // if a tree is examined but no client is there to see it, did the tree ever really exist? @@ -510,6 +511,40 @@ return TRUE +/** + * handle_eye_contact() is called when we examine() something. If we examine an alive mob with a mind who has examined us in the last 2 seconds within 5 tiles, we make eye contact! + * + * Note that if either party has their face obscured, the other won't get the notice about the eye contact + * Also note that examine_more() doesn't proc this or extend the timer, just because it's simpler this way and doesn't lose much. + * The nice part about relying on examining is that we don't bother checking visibility, because we already know they were both visible to each other within the last second, and the one who triggers it is currently seeing them + */ +/mob/proc/handle_eye_contact(mob/living/examined_mob) + return + +/mob/living/handle_eye_contact(mob/living/examined_mob) + if(!istype(examined_mob) || src == examined_mob || examined_mob.stat >= UNCONSCIOUS || !client) + return + + var/imagined_eye_contact = FALSE + if(!LAZYACCESS(examined_mob.client?.recent_examines, src)) + // even if you haven't looked at them recently, if you have the shift eyes trait, they may still imagine the eye contact + if(HAS_TRAIT(examined_mob, TRAIT_SHIFTY_EYES) && prob(10 - get_dist(src, examined_mob))) + imagined_eye_contact = TRUE + else + return + + if(get_dist(src, examined_mob) > EYE_CONTACT_RANGE) + return + + // check to see if their face is blocked or, if not, a signal blocks it + if(examined_mob.is_face_visible() && SEND_SIGNAL(src, COMSIG_MOB_EYECONTACT, examined_mob, TRUE) != COMSIG_BLOCK_EYECONTACT) + var/msg = span_smallnotice("You make eye contact with [examined_mob].") + addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, src, msg), 3) // so the examine signal has time to fire and this will print after + + if(!imagined_eye_contact && is_face_visible() && SEND_SIGNAL(examined_mob, COMSIG_MOB_EYECONTACT, src, FALSE) != COMSIG_BLOCK_EYECONTACT) + var/msg = span_smallnotice("[src] makes eye contact with you.") + addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, examined_mob, msg), 3) + /** * Point at an atom *