Files
MrMelbertandGitHub 6ab4d7766a The power of love: Obsessed are slightly stronger about their obsession (+ modernization) (#96421)
## About The Pull Request

Notable changes:

- Obsessed get a slight strength buff related to their obsession
- When grabbing any of your targets (your obsession included), your grab
is harder to escape from, and cause (slightly) more stamina damage from
failed escapes

- Obsessed also get a slight strength buff while standing near their
obsession (for at least 20 seconds). This effect is applied instantly if
the obsession is incapacitated.
- The grab strength bonus applies to all mobs, rather than just your
targets
   - You get a 25% reduction to incoming stamina damage 

- Psicodine or Alcohol will prevent Obsession stammering / emote tells,
but also prevents these beneficial effects from activating

- Witnessing the death of your obsession grants you a permanent positive
moodlet (until they are revived)

- Obsessed tells have been reworked a bit, being less obvious in general
and having their chance to trigger reduced the longer you stand around
your obsession (resetting after leaving their view)

- Obsessed stammer was also reworked a bit, now being closet to Social
Anxiety's stammer, also reducing in severity the longer you stand around
your obsession (resetting after leaving their view)

- Obsessed antag status is now lost when transferring out of the
affected brain and gained when transferring into the affected brain,
making it play nicer with slime clones or mindswap

Other changes:

- Modernized a bunch of obsession code
- Replaced hardcoded checks for grab resistance with a signal

## Why It's Good For The Game

I wanted to give Obsession just a *little* something to help enable
them. I figure grab strength and pain resistance is a pretty fitting /
flavorful place to start.

The grab strength is intended to give them more ability to detain their
targets (by means of wrestling). Even a passive grab isn't a guaranteed
escape.
The stamina damage resistance is intended to give them a slight chance
to resist being detained by security. Not a significant one, just a
slight one.

## Changelog

🆑 Melbert
balance: Obsessions get moderately stronger grabs against their
objective targets (this includes their obsession and any other
kill/theft targets they may have).
balance: While standing near their obsession (for 20 seconds or if they
are incapacitated), Obsessions get slightly stronger grabs against
anyone, rather than just their objective targets.
balance: While standing near their obsession (for 20 seconds or if they
are incapacitated), Obsessions get slight stamina damage resistance.
balance: Obsession stammering has been reduced in severity a bit, and
also becomes less likely the longer you stand around your obsession.
balance: Obsession emotes have been made a bit less obvious, and also
become less likely the longer you stand around your obsession.
balance: Taking Psicodine or drinking alcohol will prevent Obsessions
from stammering or emoting around their obsession, but also disables
their stamina resistance and stronger grabs.
balance: Obsessions get a "permanent" mood bonus for killing their
obsession (until they are revived).
refactor: Obsessed has been refactored significantly. It will now play a
lot nicer with mindswaps. Report any oddities.
refactor: Grab strength has been refactored a bit, primarily affects
martial arts and drunken brawling. Report any oddities.
/🆑
2026-06-26 12:38:13 +02:00

348 lines
12 KiB
Plaintext

/datum/status_effect/speech
id = STATUS_EFFECT_ID_ABSTRACT
alert_type = null
remove_on_fullheal = TRUE
tick_interval = STATUS_EFFECT_NO_TICK
/// If TRUE, TTS will say the original message rather than what we changed it to
var/make_tts_message_original = FALSE
/// If set, this will be appended to the TTS filter of the message
var/tts_filter = ""
/datum/status_effect/speech/on_creation(mob/living/new_owner, duration = 10 SECONDS)
src.duration = duration
return ..()
/datum/status_effect/speech/on_apply()
RegisterSignal(owner, COMSIG_LIVING_TREAT_MESSAGE, PROC_REF(handle_message))
return TRUE
/datum/status_effect/speech/on_remove()
UnregisterSignal(owner, COMSIG_LIVING_TREAT_MESSAGE)
/**
* Signal proc for [COMSIG_LIVING_TREAT_MESSAGE]
*
* Iterates over all of the characters in the passed message
* and calls apply_speech() on each.
*/
/datum/status_effect/speech/proc/handle_message(datum/source, list/message_args)
SIGNAL_HANDLER
var/phrase = html_decode(message_args[TREAT_MESSAGE_ARG])
if(!length(phrase))
return
var/final_phrase = ""
var/list/words = splittext(phrase, " ")
var/list/new_words = list()
for(var/i in 1 to length(words))
new_words += apply_speech(words[i], i)
final_phrase = jointext(new_words, " ")
if(final_phrase == phrase)
return // No change was done, whatever
if(length(tts_filter) > 0)
message_args[TREAT_TTS_FILTER_ARG] += tts_filter
if(make_tts_message_original)
message_args[TREAT_TTS_MESSAGE_ARG] = message_args[TREAT_MESSAGE_ARG]
message_args[TREAT_MESSAGE_ARG] = sanitize(final_phrase)
/**
* Applies the speech effects on the past character, changing
* the original_word into some modified_word
*
* Return the newly modified word
*/
/datum/status_effect/speech/proc/apply_speech(original_word, index)
stack_trace("[type] didn't implement apply_speech.")
return original_word
/datum/status_effect/speech/stutter
id = "stutter"
make_tts_message_original = TRUE
tts_filter = "tremolo=f=10:d=0.8,rubberband=tempo=0.5"
/// The probability of adding a stutter to any word
var/stutter_prob = 75
/// The chance of a four character stutter
var/four_char_chance = 10
/// The chance of a three character stutter
var/three_char_chance = 20
/// The chance of a two character stutter
var/two_char_chance = 90
/// Regex to apply generically to stuttered words
/// * 1st capture group is any leading invalid characters (can be empty)
/// * 2nd capture group is either a digraph (th, qu, ch) or a consonant
/// * 3rd capture group is the rest of the word (can be empty)
VAR_FINAL/static/regex/stutter_regex
/datum/status_effect/speech/stutter/on_creation(mob/living/new_owner, ...)
stutter_regex ||= regex(@@^([\s"'()[\]{}.!?,:;_`~-]*\b)([^aeoiuh\d]h|qu|[^\d])(.*)@, "i")
return ..()
/datum/status_effect/speech/stutter/apply_speech(original_word, index)
if(!prob(stutter_prob))
return original_word
if(stutter_regex.Find(original_word))
return "[stutter_regex.group[1]][stutter_char(stutter_regex.group[2])][stutter_regex.group[3]]"
return original_word // i give up
/datum/status_effect/speech/stutter/proc/stutter_char(some_char)
if(prob(four_char_chance))
return "[some_char]-[some_char]-[some_char]-[some_char]"
if(prob(three_char_chance))
return "[some_char]-[some_char]-[some_char]"
if(prob(two_char_chance))
return "[some_char]-[some_char]"
return some_char
/datum/status_effect/speech/stutter/anxiety
id = "anxiety_stutter"
stutter_prob = 5
four_char_chance = 4
three_char_chance = 10
two_char_chance = 100
remove_on_fullheal = FALSE
/datum/status_effect/speech/stutter/anxiety/handle_message(datum/source, list/message_args)
if(HAS_TRAIT(owner, TRAIT_FEARLESS) || HAS_TRAIT(owner, TRAIT_SIGN_LANG))
stutter_prob = 0
else
var/datum/quirk/social_anxiety/host_quirk = owner.get_quirk(/datum/quirk/social_anxiety)
stutter_prob = clamp(host_quirk?.calculate_mood_mod() * 0.5, 5, 50)
return ..()
/datum/status_effect/speech/stutter/obsession
id = "obsession_stutter"
stutter_prob = 25
four_char_chance = 5
three_char_chance = 25
two_char_chance = 100
remove_on_fullheal = FALSE
COOLDOWN_DECLARE(stutter_cooldown)
/datum/status_effect/speech/stutter/obsession/handle_message(datum/source, list/message_args)
if(should_stutter())
var/datum/brain_trauma/special/obsessed/obsession_trauma = get_obsession()
stutter_prob = initial(stutter_prob)
four_char_chance = initial(four_char_chance) * max(1 - (obsession_trauma.time_spend_creeping / (10 SECONDS)), 0.01)
three_char_chance = initial(three_char_chance) * max(1 - (obsession_trauma.time_spend_creeping / (20 SECONDS)), 0.01)
two_char_chance = initial(two_char_chance) * max(1 - (obsession_trauma.time_spend_creeping / (40 SECONDS)), 0.01)
else
stutter_prob = 0
return ..()
/datum/status_effect/speech/stutter/obsession/apply_speech(original_word, index)
. = ..()
if(. == original_word || !should_stutter())
return
var/datum/brain_trauma/special/obsessed/obsession_trauma = get_obsession()
to_chat(owner, span_warning("Being near [obsession_trauma.obsession.real_name] makes you nervous and stutter..."))
COOLDOWN_START(src, stutter_cooldown, rand(4, 8) SECONDS)
/datum/status_effect/speech/stutter/obsession/proc/get_obsession()
if(!ishuman(owner))
return null
var/mob/living/carbon/human/human_owner = owner
return human_owner.has_trauma_type(/datum/brain_trauma/special/obsessed)
/datum/status_effect/speech/stutter/obsession/proc/should_stutter()
if(HAS_TRAIT(owner, TRAIT_FEARLESS))
return FALSE
if(!COOLDOWN_FINISHED(src, stutter_cooldown))
return FALSE
var/datum/brain_trauma/special/obsessed/obsession_trauma = get_obsession()
if(isnull(obsession_trauma) || !obsession_trauma.viewing || obsession_trauma.witnessed_death)
return FALSE
return TRUE
/datum/status_effect/speech/stutter/derpspeech
id = "derp_stutter"
/// The probability of making our message entirely uppercase + adding exclamations
var/capitalize_prob = 50
/// The probability of adding a stutter to the entire message, if we're not already stuttering
var/message_stutter_prob = 15
/datum/status_effect/speech/stutter/derpspeech/handle_message(datum/source, list/message_args)
var/message = html_decode(message_args[TREAT_MESSAGE_ARG])
message = replacetext(message, " am ", " ")
message = replacetext(message, " is ", " ")
message = replacetext(message, " are ", " ")
message = replacetext(message, "you", "u")
message = replacetext(message, "help", "halp")
message = replacetext(message, "grief", "grife")
message = replacetext(message, "space", "spess")
message = replacetext(message, "carp", "crap")
message = replacetext(message, "reason", "raisin")
if(prob(capitalize_prob))
var/exclamation = pick("!", "!!", "!!!")
message = uppertext(message)
message += "[stutter_char(exclamation)]"
message_args[TREAT_MESSAGE_ARG] = message
var/mob/living/living_source = source
if(!isliving(source) || living_source.has_status_effect(/datum/status_effect/speech/stutter))
return
// If we're not stuttering, we have a chance of calling parent here, adding stutter effects
if(prob(message_stutter_prob))
return ..()
// Otherwise just return and don't call parent, we already modified our speech
return
/datum/status_effect/speech/slurring
/// The chance that any given character in a message will be replaced with a common character
var/common_prob = 25
/// The chance that any given character in a message will be replaced with an uncommon character
var/uncommon_prob = 10
/// The chance that any given character will be entirely replaced with a new string / will have a string appended onto it
var/replacement_prob = 5
/// The chance that any given character will be doubled, or even tripled
var/doubletext_prob = 0
/// The file we pull text modifications from
var/text_modification_file = ""
/// Common replacements for characters - populated in on_creation
var/list/common_replacements
/// Uncommon replacements for characters - populated in on_creation
var/list/uncommon_replacements
/// Strings that fully replace a character - populated in on_creation
var/list/string_replacements
/// Strings that are appended to a character - populated in on_creation
var/list/string_additions
/// Regex for characters we won't apply any slurring to
var/static/regex/no_slur
/// If the last character we processed was a replacement, don't do another replacement right after it
VAR_PRIVATE/replacement_dupe_check = FALSE
/datum/status_effect/speech/slurring/on_creation(mob/living/new_owner, duration = 10 SECONDS)
. = ..()
if(!.)
return
if(!text_modification_file)
CRASH("[type] was created without a text modification file.")
var/list/speech_changes = strings(text_modification_file, "replacements")
common_replacements = speech_changes["characters"]["common"]
uncommon_replacements = speech_changes["characters"]["uncommon"]
string_replacements = speech_changes["string_replacements"]
string_additions = speech_changes["string_additions"]
no_slur ||= regex(@@[ "'()[\]{}.!?,:;_`~-]@)
/datum/status_effect/speech/slurring/apply_speech(original_word, index)
var/original_char = ""
var/modified_word = ""
for(var/i = 1, i <= length(original_word), i += length(original_char))
original_char = original_word[i]
modified_word += slur_character(original_char, i)
return modified_word
/datum/status_effect/speech/slurring/proc/slur_character(original_char, index)
var/modified_char = original_char
var/allow_slurring = index != 1 && !no_slur.Find(modified_char)
var/lower_char = LOWER_TEXT(modified_char)
if(prob(common_prob) && (lower_char in common_replacements))
var/to_replace = common_replacements[lower_char]
modified_char = islist(to_replace) ? pick(to_replace) : to_replace
replacement_dupe_check = FALSE
else if(prob(uncommon_prob) && (modified_char in uncommon_replacements))
var/to_replace = uncommon_replacements[modified_char]
modified_char = islist(to_replace) ? pick(to_replace) : to_replace
replacement_dupe_check = FALSE
else if(allow_slurring) // Don't do replacements on the first character, or punctuation
if(!replacement_dupe_check && prob(replacement_prob))
var/replacements_len = length(string_replacements)
var/additions_len = length(string_additions)
if(replacements_len && additions_len)
// Calculate the probability of grabbing a replacement vs an addition
var/weight = (replacements_len + additions_len) / replacements_len * 100
if(prob(weight))
modified_char = pick(string_replacements)
else
modified_char += pick(string_additions)
else if(replacements_len)
modified_char = pick(string_replacements)
else if(additions_len)
modified_char += pick(string_additions)
replacement_dupe_check = TRUE
else if(prob(doubletext_prob))
modified_char += "[modified_char][prob(50) ? "" : modified_char]"
replacement_dupe_check = FALSE
else
// If we don't allow replacements we don't want the next character to be a replacement either
// This makes some more coherent speech by disallowing structures like "Y'''e"
replacement_dupe_check = TRUE
return modified_char
/datum/status_effect/speech/slurring/generic
id = "generic_slurring"
common_prob = 33
uncommon_prob = 0
replacement_prob = 24
doubletext_prob = 40
text_modification_file = "slurring_drunk_text.json"
/datum/status_effect/speech/slurring/drunk
id = "drunk_slurring"
// These defaults are updated when speech event occur.
common_prob = -1
uncommon_prob = -1
replacement_prob = -1
doubletext_prob = -1
text_modification_file = "slurring_drunk_text.json"
/datum/status_effect/speech/slurring/drunk/handle_message(datum/source, list/message_args)
var/current_drunkness = owner.get_drunk_amount()
// These numbers are arbitarily picked
// Common replacements start at about 20, and maxes out at about 85
common_prob = clamp((current_drunkness * 0.8) - 16, 4, 50)
// Uncommon replacements (burping) start at 50 and max out at 110 (when you are dying)
uncommon_prob = clamp((current_drunkness * 0.3) - 10, 0, 12)
// Replacements start at 20 and max out at about 60
replacement_prob = clamp((current_drunkness * 0.6) - 8, 0, 12)
// Double texting start out at about 25 and max out at about 60
doubletext_prob = clamp((current_drunkness * 0.8) - 8, 2, 50)
return ..()
/datum/status_effect/speech/slurring/cult
id = "cult_slurring"
common_prob = 50
uncommon_prob = 25
replacement_prob = 33
doubletext_prob = 0
text_modification_file = "slurring_cult_text.json"
tts_filter = "rubberband=pitch=0.5,vibrato=5"
/datum/status_effect/speech/slurring/heretic
id = "heretic_slurring"
common_prob = 50
uncommon_prob = 20
replacement_prob = 30
doubletext_prob = 5
text_modification_file = "slurring_heretic_text.json"