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.
/🆑
This commit is contained in:
MrMelbert
2026-06-26 12:38:13 +02:00
committed by GitHub
parent d7b39b81c9
commit 6ab4d7766a
10 changed files with 381 additions and 184 deletions
@@ -86,6 +86,11 @@
#define COMSIG_MOVABLE_SET_ANCHORED "movable_set_anchored"
///from base of atom/movable/setGrabState(): (newstate)
#define COMSIG_MOVABLE_SET_GRAB_STATE "living_set_grab_state"
/// from base of mob/living/resist_grab(), to the grabber: (mob/living/grabbed, list/grab_stats)
#define COMSIG_MOVABLE_GRABBED_RESISTING "movable_grabbed_resisting"
#define GRAB_STAT_EFFECTIVE_STATE 1
#define GRAB_STAT_FAIL_DAMAGE 2
#define GRAB_STAT_ESCAPE_CHANCE 3
///called when the movable's glide size is updated: (new_glide_size)
#define COMSIG_MOVABLE_UPDATE_GLIDE_SIZE "movable_glide_size"
///Called when a movable is hit by a plunger in layer mode, from /obj/item/plunger/attack_atom()
+164 -86
View File
@@ -10,14 +10,20 @@
can_gain = TRUE
random_gain = FALSE
resilience = TRAUMA_RESILIENCE_LOBOTOMY
var/mob/living/obsession
var/datum/objective/spendtime/attachedobsessedobj
var/datum/antagonist/obsessed/antagonist
var/viewing = FALSE //it's a lot better to store if the owner is watching the obsession than checking it twice between two procs
var/total_time_creeping = 0 SECONDS //just for round end fun
var/time_spent_away = 0 SECONDS
var/obsession_hug_count = 0
/// Reference to the actual mob we're obsessed with
VAR_FINAL/mob/living/obsession
/// Reference to our antag datum
VAR_FINAL/datum/antagonist/obsessed/antagonist
/// Tracks if the target is currently in view
VAR_FINAL/viewing = FALSE
/// Tracks the total amount of time spend near the target
VAR_FINAL/total_time_creeping = 0 SECONDS
/// Tracks the current period of time spent away from the target (resetting when near)
VAR_FINAL/time_spent_away = 0 SECONDS
/// Tracks the current period of time spent near the target (resetting when away)
VAR_FINAL/time_spend_creeping = 0 SECONDS
/// If we've seen our obsession be dead
VAR_FINAL/witnessed_death = FALSE
/datum/brain_trauma/special/obsessed/on_gain()
//setup, linking, etc//
@@ -26,66 +32,90 @@
if(!obsession)//we didn't find one
lose_text = ""
return FALSE
gain_text = span_warning("You hear a sickening, raspy voice in your head. It wants one small task of you...")
owner.mind.add_antag_datum(/datum/antagonist/obsessed)
antagonist = owner.mind.has_antag_datum(/datum/antagonist/obsessed)
antagonist = owner.mind.add_antag_datum(/datum/antagonist/obsessed)
antagonist.trauma = src
RegisterSignal(obsession, COMSIG_MOB_EYECONTACT, PROC_REF(stare))
RegisterSignal(obsession, COMSIG_QDELETING, PROC_REF(obession_deleted))
. = ..()
//antag stuff//
antagonist.forge_objectives(obsession.mind)
antagonist.greet()
log_game("[key_name(antagonist)] has developed an obsession with [key_name(obsession)].")
RegisterSignal(owner, COMSIG_CARBON_HELPED, PROC_REF(on_hug))
RegisterSignal(owner, COMSIG_MOVABLE_GRABBED_RESISTING, PROC_REF(grab_resisting))
RegisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(on_damage_mod))
RegisterSignal(owner, COMSIG_MOB_MIND_TRANSFERRED_OUT_OF, PROC_REF(on_mind_lost))
RegisterSignal(owner, COMSIG_MOB_MIND_TRANSFERRED_INTO, PROC_REF(on_mind_gain))
owner.apply_status_effect(/datum/status_effect/desensitized, REF(src), DESENSITIZED_THRESHOLD)
owner.apply_status_effect(/datum/status_effect/speech/stutter/obsession, INFINITY)
/datum/brain_trauma/special/obsessed/on_life(seconds_per_tick)
if(!obsession || obsession.stat == DEAD)
viewing = FALSE//important, makes sure you no longer stutter when happy if you murdered them while viewing
return
if(get_dist(get_turf(owner), get_turf(obsession)) > 7)
viewing = FALSE //they are further than our view range they are not viewing us
out_of_view()
return//so we're not searching everything in view every tick
if(obsession in view(7, owner))
viewing = TRUE
else
if(isnull(obsession))
viewing = FALSE
if(viewing)
owner.add_mood_event("creeping", /datum/mood_event/creeping, obsession.name)
total_time_creeping += seconds_per_tick SECONDS
time_spent_away = 0 SECONDS
if(attachedobsessedobj)//if an objective needs to tick down, we can do that since traumas coexist with the antagonist datum
attachedobsessedobj.timer -= seconds_per_tick SECONDS //mob subsystem ticks every 2 seconds(?), remove 20 deciseconds from the timer. sure, that makes sense.
else
out_of_view()
return
/datum/brain_trauma/special/obsessed/proc/out_of_view()
time_spent_away += 2 SECONDS
if(time_spent_away > 3 MINUTES) //3 minutes
owner.add_mood_event("creeping", /datum/mood_event/notcreepingsevere, obsession.name)
else
owner.add_mood_event("creeping", /datum/mood_event/notcreeping, obsession.name)
// viewing needs to be updated regardless of the obsession's state
viewing = IN_GIVEN_RANGE(owner, obsession, 7) && (owner in viewers(7, obsession))
if(!viewing)
if(witnessed_death)
return
time_spent_away += seconds_per_tick SECONDS
time_spend_creeping = 0 SECONDS
if(time_spent_away > 3 MINUTES) //3 minutes
owner.add_mood_event("creeping", /datum/mood_event/notcreepingsevere, obsession.name)
else
owner.add_mood_event("creeping", /datum/mood_event/notcreeping, obsession.name)
return
if(obsession.stat == DEAD)
if(!witnessed_death)
witnessed_death = TRUE
owner.add_mood_event("creeping", /datum/mood_event/creeping/dead)
return
witnessed_death = FALSE
owner.add_mood_event("creeping", /datum/mood_event/creeping, obsession.name)
total_time_creeping += seconds_per_tick SECONDS
time_spend_creeping += seconds_per_tick SECONDS
time_spent_away = 0 SECONDS
for(var/datum/objective/spendtime/objective in antagonist?.objectives)
objective.timer -= seconds_per_tick SECONDS
/datum/brain_trauma/special/obsessed/on_lose()
..()
. = ..()
antagonist?.trauma = null
antagonist = null
if (owner.mind.remove_antag_datum(/datum/antagonist/obsessed))
owner.mind.add_antag_datum(/datum/antagonist/former_obsessed)
owner.clear_mood_event("creeping")
if(obsession)
if(!isnull(obsession))
log_game("[key_name(owner)] is no longer obsessed with [key_name(obsession)].")
UnregisterSignal(obsession, COMSIG_MOB_EYECONTACT)
UnregisterSignal(obsession, list(
COMSIG_MOB_EYECONTACT,
COMSIG_QDELETING,
))
obsession = null
UnregisterSignal(owner, list(
COMSIG_CARBON_HELPED,
COMSIG_MOVABLE_GRABBED_RESISTING,
COMSIG_MOB_APPLY_DAMAGE_MODIFIERS,
COMSIG_MOB_MIND_TRANSFERRED_INTO,
COMSIG_MOB_MIND_TRANSFERRED_OUT_OF,
))
owner.remove_status_effect(/datum/status_effect/desensitized, REF(src))
owner.remove_status_effect(/datum/status_effect/speech/stutter/obsession)
/datum/brain_trauma/special/obsessed/proc/obession_deleted(datum/source)
SIGNAL_HANDLER
obsession = null
/datum/brain_trauma/special/obsessed/handle_speech(datum/source, list/speech_args)
if(!viewing)
return
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_REF(on_failed_social_interaction)), rand(1 SECONDS, 3 SECONDS))
else if(!owner.has_status_effect(/datum/status_effect/speech/stutter))
to_chat(owner, span_warning("Being near [obsession] makes you nervous and you begin to stutter..."))
owner.set_stutter_if_lower(6 SECONDS)
if(viewing && !witnessed_death && prob(12 * max(1 - (time_spend_creeping / (40 SECONDS)), 0.02) ))
addtimer(CALLBACK(src, PROC_REF(do_something_nervous)), rand(1 SECONDS, 3 SECONDS))
/// Singal proc for [COMSIG_CARBON_HELPED], when our obsessed helps (hugs) our obsession, increases hug count
/datum/brain_trauma/special/obsessed/proc/on_hug(datum/source, mob/living/hugged)
@@ -94,26 +124,70 @@
if(hugged != obsession)
return
obsession_hug_count++
for(var/datum/objective/hug/objective in antagonist?.objectives)
objective.hugs_needed -= 1
/datum/brain_trauma/special/obsessed/proc/on_failed_social_interaction()
if(QDELETED(owner) || owner.stat >= UNCONSCIOUS)
/// Signal proc for [COMSIG_MOVABLE_GRABBED_RESISTING] to improve the obsessed's grabs
/datum/brain_trauma/special/obsessed/proc/grab_resisting(datum/source, mob/living/grabbed, list/grab_stats)
SIGNAL_HANDLER
if(HAS_TRAIT(owner, TRAIT_FEARLESS))
return
switch(rand(1, 100))
if(1 to 40)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), pick("blink", "blink_r"))
owner.set_eye_blur_if_lower(20 SECONDS)
to_chat(owner, span_userdanger("You sweat profusely and have a hard time focusing..."))
if(41 to 80)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "pale")
shake_camera(owner, 15, 1)
owner.adjust_stamina_loss(70)
to_chat(owner, span_userdanger("You feel your heart lurching in your chest..."))
if(81 to 100)
var/list/datum/mind/all_targets = list()
for(var/datum/objective/objective in antagonist?.objectives)
if(isnull(objective.target))
continue
all_targets |= objective.target
// We always grab our obsession, or similar targets, with extra strength
if(grabbed == obsession || (grabbed.mind in all_targets))
grab_stats[GRAB_STAT_EFFECTIVE_STATE] += 1
grab_stats[GRAB_STAT_FAIL_DAMAGE] += 5
// If we're hanging with our obsession for a while, the bonus applies to any mob (though to a lesser extent)
else if(is_defensive())
grab_stats[GRAB_STAT_EFFECTIVE_STATE] += 1
grab_stats[GRAB_STAT_ESCAPE_CHANCE] += 10 // 10% EASIER to escape
/// Signal proc for [COMSIG_MOB_APPLY_DAMAGE_MODIFIERS], take less stamina damage when near our obsession
/datum/brain_trauma/special/obsessed/proc/on_damage_mod(datum/source, list/damage_mods, damage, damage_type, ...)
SIGNAL_HANDLER
if(HAS_TRAIT(owner, TRAIT_FEARLESS))
return
if(damage_type == STAMINA && is_defensive())
damage_mods += 0.75
/// Checks if if we're being defensive over our obsession
/datum/brain_trauma/special/obsessed/proc/is_defensive()
if(time_spend_creeping >= 20 SECONDS)
return TRUE
if(obsession.stat >= UNCONSCIOUS)
return (owner in viewers(7, obsession))
return FALSE
/datum/brain_trauma/special/obsessed/proc/do_something_nervous()
if(QDELETED(owner) || owner.stat >= UNCONSCIOUS || HAS_TRAIT(owner, TRAIT_FEARLESS))
return
switch(rand(1, 10))
if(1 to 4)
owner.adjust_jitter_up_to(10 SECONDS, 20 SECONDS)
owner.adjust_dizzy_up_to(10 SECONDS, 20 SECONDS)
to_chat(owner, span_warning("You feel a bit nervous."))
if(5 to 8)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "cough")
owner.adjust_dizzy(20 SECONDS)
owner.adjust_disgust(5)
to_chat(owner, span_userdanger("You gag and swallow a bit of bile..."))
to_chat(owner, span_warning("You clear your throat."))
if(9)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "laugh")
to_chat(owner, span_warning("You chuckle nervously."))
if(10)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "blink")
owner.adjust_eye_blur_up_to(10 SECONDS, 20 SECONDS)
to_chat(owner, span_warning("You forget to blink for a moment."))
// if the creep examines first, then the obsession examines them, have a 50% chance to possibly blow their cover. wearing a mask avoids this risk
/datum/brain_trauma/special/obsessed/proc/stare(datum/source, mob/living/examining_mob, triggering_examiner)
@@ -125,36 +199,40 @@
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(to_chat), obsession, span_warning("You catch [examining_mob] staring at you..."), 3))
return COMSIG_BLOCK_EYECONTACT
/datum/brain_trauma/special/obsessed/proc/on_mind_lost(datum/source, mob/new_body, datum/mind/the_mind)
SIGNAL_HANDLER
INVOKE_ASYNC(antagonist, TYPE_PROC_REF(/datum/antagonist, store_datum))
/datum/brain_trauma/special/obsessed/proc/on_mind_gain(datum/source, mob/new_body, datum/mind/the_mind)
SIGNAL_HANDLER
INVOKE_ASYNC(antagonist, TYPE_PROC_REF(/datum/antagonist, restore_datum), the_mind)
/datum/brain_trauma/special/obsessed/proc/find_obsession()
var/list/viable_minds = list() //The first list, which excludes hijinks
var/list/possible_targets = list() //The second list, which filters out silicons and simplemobs
var/static/list/trait_obsessions = list(
var/list/generic_pool = list()
var/list/special_pool = list()
var/list/trait_obsessions = list(
JOB_MIME = TRAIT_MIME_FAN,
JOB_CLOWN = TRAIT_CLOWN_ENJOYER,
JOB_CHAPLAIN = TRAIT_SPIRITUAL,
) // Jobs and their corresponding quirks
var/list/special_pool = list() //The special list, for quirk-based
var/chosen_victim //The obsession target
)
for(var/mob/player as anything in GLOB.player_list)//prevents crew members falling in love with nuke ops they never met, and other annoying hijinks
if(!player.client || !player.mind || isnewplayer(player) || player.stat == DEAD || isbrain(player) || player == owner)
for(var/datum/mind/crewmember as anything in get_crewmember_minds())
if(!ishuman(crewmember.current) || crewmember.current.stat == DEAD || !GET_CLIENT(crewmember.current))
continue
if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER))
continue
viable_minds += player.mind
for(var/datum/mind/possible_target as anything in viable_minds)
if(possible_target != owner && ishuman(possible_target.current))
var/job = possible_target.assigned_role.title
if (trait_obsessions[job] != null && HAS_TRAIT(owner, trait_obsessions[job]))
special_pool += possible_target.current
possible_targets += possible_target.current
//Do we have any special target?
var/job = crewmember.assigned_role.title
if (trait_obsessions[job] && HAS_MIND_TRAIT(owner, trait_obsessions[job]))
special_pool += crewmember.current
generic_pool += crewmember.current
if(length(special_pool))
chosen_victim = pick(special_pool)
return chosen_victim
return pick(special_pool)
//If not, pick any other ordinary target
if(possible_targets.len > 0)
chosen_victim = pick(possible_targets)
return chosen_victim
if(length(generic_pool))
return pick(generic_pool)
return null
+20 -1
View File
@@ -119,6 +119,17 @@
return grab_act(source, grabbing)
/// Signal proc for [COMSIG_MOVABLE_GRABBED_RESISTING] to modify grab stats
/datum/martial_art/proc/grabbed_resisting(mob/living/source, mob/living/grabbed, list/grab_stats)
SIGNAL_HANDLER
if(!can_use(source))
return
grab_stats[GRAB_STAT_EFFECTIVE_STATE] += grab_state_modifier
grab_stats[GRAB_STAT_FAIL_DAMAGE] += grab_damage_modifier
grab_stats[GRAB_STAT_ESCAPE_CHANCE] += grab_escape_chance_modifier
/**
* Called when help-intenting on someone
*
@@ -384,6 +395,7 @@
active = TRUE
RegisterSignal(new_holder, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(unarmed_strike))
RegisterSignal(new_holder, COMSIG_LIVING_GRAB, PROC_REF(attempt_grab))
RegisterSignal(new_holder, COMSIG_MOVABLE_GRABBED_RESISTING, PROC_REF(grabbed_resisting))
RegisterSignals(new_holder, list(COMSIG_LIVING_TABLE_SLAMMING, COMSIG_LIVING_TABLE_LIMB_SLAMMING), PROC_REF(smash_table))
if(display_combos)
RegisterSignal(new_holder, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
@@ -396,7 +408,14 @@
/datum/martial_art/proc/deactivate_style(mob/living/remove_from)
SHOULD_CALL_PARENT(TRUE)
active = FALSE
UnregisterSignal(remove_from, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_LIVING_GRAB, COMSIG_LIVING_TABLE_SLAMMING, COMSIG_LIVING_TABLE_LIMB_SLAMMING))
UnregisterSignal(remove_from, list(
COMSIG_LIVING_UNARMED_ATTACK,
COMSIG_LIVING_GRAB,
COMSIG_LIVING_TABLE_SLAMMING,
COMSIG_LIVING_TABLE_LIMB_SLAMMING,
COMSIG_MOVABLE_GRABBED_RESISTING,
COMSIG_MOB_HUD_CREATED,
))
remove_from.hud_used?.remove_screen_object(HUD_MOB_COMBO)
///Gives the owner of the martial art the combo HUD.
+12 -8
View File
@@ -30,16 +30,20 @@
/datum/mind/proc/remove_antag_datum(datum_type)
if(!datum_type)
return
var/datum/antagonist/A = has_antag_datum(datum_type)
if(A)
A.on_removal()
current?.log_message("has lost antag datum [A.name]([A.type]).", LOG_GAME)
return TRUE
var/datum/antagonist/antag = has_antag_datum(datum_type)
if(isnull(antag))
return
antag.on_removal()
qdel(antag)
current?.log_message("has lost antag datum [antag] ([antag.type]).", LOG_GAME)
return TRUE
/datum/mind/proc/remove_all_antag_datums() //For the Lazy amongst us.
for(var/a in antag_datums)
var/datum/antagonist/A = a
A.on_removal()
for(var/datum/antagonist/antag as anything in antag_datums)
antag.on_removal()
qdel(antag)
current?.log_message("has lost all antag datums.", LOG_GAME)
/datum/mind/proc/has_antag_datum(datum_type, check_subtypes = TRUE)
@@ -230,6 +230,10 @@
timeout = 3 SECONDS
hidden = TRUE
/datum/mood_event/creeping/dead
mood_change = 8
timeout = 0
/datum/mood_event/revolution
description = "VIVA LA REVOLUTION!"
mood_change = 3
@@ -105,6 +105,7 @@
owner.add_mood_event(id, /datum/mood_event/drunk, drunk_value)
owner.clear_mood_event("[id]_after")
RegisterSignal(owner, COMSIG_MOB_FIRED_GUN, PROC_REF(drunk_gun_fired))
RegisterSignal(owner, COMSIG_MOVABLE_GRABBED_RESISTING, PROC_REF(grabbed_resisting))
/datum/status_effect/inebriated/drunk/on_remove()
clear_effects()
@@ -125,6 +126,7 @@
owner.sound_environment_override = SOUND_ENVIRONMENT_NONE
UnregisterSignal(owner, COMSIG_MOB_FIRED_GUN)
UnregisterSignal(owner, COMSIG_MOVABLE_GRABBED_RESISTING)
REMOVE_TRAIT(owner, TRAIT_FEARLESS, TRAIT_STATUS_EFFECT(id))
/datum/status_effect/inebriated/drunk/proc/drunk_gun_fired(datum/source, obj/item/gun/gun, atom/firing_at, params, zone, bonus_spread_values)
@@ -138,6 +140,15 @@
return
bonus_spread_values[MAX_BONUS_SPREAD_INDEX] += (drunk_value * 0.5)
/datum/status_effect/inebriated/drunk/proc/grabbed_resisting(datum/source, mob/living/grabbed, list/grab_stats)
SIGNAL_HANDLER
if(!HAS_TRAIT(owner, TRAIT_DRUNKEN_BRAWLER))
return
grab_stats[GRAB_STAT_EFFECTIVE_STATE] += 1
grab_stats[GRAB_STAT_FAIL_DAMAGE] += clamp((owner.get_fire_loss() + owner.get_brute_loss()) / 10, 3, 20)
/datum/status_effect/inebriated/drunk/set_drunk_value(set_to)
. = ..()
if(QDELETED(src))
@@ -64,7 +64,7 @@
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 character
/// 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
@@ -117,6 +117,52 @@
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
@@ -343,7 +343,6 @@ GLOBAL_LIST_EMPTY(antagonists)
SEND_SIGNAL(owner, COMSIG_ANTAGONIST_REMOVED, src)
if(owner.current)
SEND_SIGNAL(owner.current, COMSIG_MOB_ANTAGONIST_REMOVED, src)
qdel(src)
/**
* Proc that sends fluff or instructional messages to the player when they are given this antag datum.
@@ -628,3 +627,34 @@ GLOBAL_LIST_EMPTY(antagonists)
/// Return TRUE to prevent the antag's job from handling the respawn
/datum/antagonist/proc/on_respawn(mob/new_character)
return FALSE
/// Dissassociates the antag datum from its owner, without deleting it - allowing one datum and its objectives to be reused for another mind
/datum/antagonist/proc/store_datum()
if(isnull(owner))
stack_trace("Tried to store an antagonist datum that already has no owner.")
return FALSE
on_removal()
var/datum/team/antag_team = get_team()
antag_team?.remove_member(owner)
LAZYREMOVE(owner.antag_datums, src)
owner = null
log_game("[key_name(owner)] has lost antag datum [src] ([type]).")
QDEL_NULL(team_hud_ref)
return TRUE
/// Reassociates the antag datum with a new mind - allowing one datum and its objectives to be reused for another mind
/datum/antagonist/proc/restore_datum(datum/mind/new_owner)
if(!isnull(owner))
stack_trace("Tried to restore an antagonist datum that already has an owner.")
return FALSE
if(!can_be_owned(new_owner))
return FALSE
owner = new_owner
LAZYADD(owner.antag_datums, src)
var/datum/team/antag_team = get_team()
antag_team?.add_member(new_owner)
on_gain()
log_game("[key_name(new_owner)] has gained antag datum [src] ([type]).")
return TRUE
+71 -67
View File
@@ -24,6 +24,9 @@
/// Brain trauma that causes the obsession
var/datum/brain_trauma/special/obsessed/trauma
/datum/antagonist/obsessed/can_be_owned(datum/mind/new_owner)
return ..() && new_owner.current?.get_organ_by_type(/obj/item/organ/brain) // gotta have a brain to be obsessed!
/// Dummy antag datum that will show the cured obsessed to admins
/datum/antagonist/former_obsessed
name = "Former Obsessed"
@@ -34,6 +37,7 @@
antag_flags = ANTAG_FAKE|ANTAG_SKIP_GLOBAL_LIST
silent = TRUE
can_elimination_hijack = ELIMINATION_PREVENT
ui_name = null
/datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin)
var/mob/living/carbon/C = new_owner.current
@@ -53,9 +57,8 @@
owner.announce_objectives()
/datum/antagonist/obsessed/Destroy()
if(trauma)
qdel(trauma)
. = ..()
QDEL_NULL(trauma)
return ..()
/datum/antagonist/obsessed/get_preview_icon()
var/datum/universal_icon/obsessed_icon = render_preview_outfit(preview_outfit)
@@ -94,14 +97,8 @@
var/datum/objective/assassinate/obsessed/kill = new
kill.owner = owner
kill.target = obsessionmind
var/obj/family_heirloom
for(var/datum/quirk/quirky in obsessionmind.current.quirks)
if(istype(quirky, /datum/quirk/item_quirk/family_heirloom))
var/datum/quirk/item_quirk/family_heirloom/heirloom_quirk = quirky
family_heirloom = heirloom_quirk.heirloom?.resolve()
break
if(family_heirloom)
if(obsessionmind.current?.has_quirk(/datum/quirk/item_quirk/family_heirloom))
objectives_left += OBSESSED_OBJECTIVE_HEIRLOOM
// If they have no coworkers, jealousy will pick someone else on the station. This will never be a free objective.
@@ -120,6 +117,7 @@
var/datum/objective/polaroid/polaroid = new
polaroid.owner = owner
polaroid.target = obsessionmind
polaroid.obsession_weakref = WEAKREF(obsessionmind.current)
objectives += polaroid
if(OBSESSED_OBJECTIVE_HUG)
var/datum/objective/hug/hug = new
@@ -129,13 +127,14 @@
if(OBSESSED_OBJECTIVE_HEIRLOOM)
var/datum/objective/steal/heirloom_thief/heirloom_thief = new
heirloom_thief.owner = owner
heirloom_thief.target = obsessionmind//while you usually wouldn't need this for stealing, we need the name of the obsession
heirloom_thief.steal_target = family_heirloom
heirloom_thief.target = obsessionmind
heirloom_thief.find_target()
objectives += heirloom_thief
if(OBSESSED_OBJECTIVE_JEALOUS)
var/datum/objective/assassinate/jealous/jealous = new
jealous.owner = owner
jealous.target = obsessionmind//will reroll into a coworker on the objective itself
jealous.obsessed_target = obsessionmind
jealous.find_target()
objectives += jealous
objectives += kill//finally add the assassinate last, because you'd have to complete it last to greentext.
@@ -183,107 +182,96 @@
/datum/objective/assassinate/obsessed //just a creepy version of assassinate
/datum/objective/assassinate/obsessed/update_explanation_text()
..()
if(target?.current)
explanation_text = "Murder [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]."
explanation_text = "Murder [target.name], the [target_role_type ? english_list(target.get_special_roles()) : target.assigned_role.title]."
else
message_admins("WARNING! [ADMIN_LOOKUPFLW(owner)] obsessed objectives forged without an obsession!")
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/assassinate/jealous //assassinate, but it changes the target to someone else in the previous target's department. cool, right?
var/datum/mind/old //the target the coworker was picked from.
var/datum/mind/obsessed_target
/datum/objective/assassinate/jealous/update_explanation_text()
..()
old = find_coworker(target)
if(target?.current && old)
explanation_text = "Murder [target.name], [old]'s coworker."
if(target?.current && obsessed_target)
explanation_text = "Murder [target.name], [obsessed_target]'s coworker."
else
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/assassinate/jealous/find_target(dupe_search_range, list/blacklist)
if(is_unassigned_job(obsessed_target.assigned_role))
return ..()
/datum/objective/assassinate/jealous/proc/find_coworker(datum/mind/oldmind)//returning null = free objective
if(is_unassigned_job(oldmind.assigned_role))
return
var/list/viable_coworkers = list()
var/list/all_coworkers = list()
var/our_departments = oldmind.assigned_role.departments_bitflags
for(var/mob/living/carbon/human/human_alive in GLOB.alive_mob_list)
if(!human_alive.mind)
continue
if(human_alive == oldmind.current || human_alive.mind.assigned_role.faction != FACTION_STATION || human_alive.mind.has_antag_datum(/datum/antagonist/obsessed))
continue //the jealousy target has to have a job, and not be the obsession or obsessed.
all_coworkers += human_alive.mind
if(!(our_departments & human_alive.mind.assigned_role.departments_bitflags))
continue
viable_coworkers += human_alive.mind
var/our_departments = obsessed_target.assigned_role.departments_bitflags
for(var/datum/mind/crewmember as anything in get_crewmember_minds())
if(crewmember == obsessed_target || crewmember.has_antag_datum(/datum/antagonist/obsessed))
continue // the jealousy target has to have a job, and not be the obsession or obsessed.
if(length(viable_coworkers))//find someone in the same department
if(our_departments & crewmember.assigned_role.departments_bitflags)
viable_coworkers += crewmember
all_coworkers += crewmember
if(length(viable_coworkers)) // find someone in the same department
target = pick(viable_coworkers)
else if(length(all_coworkers))//find someone who works on the station
else if(length(all_coworkers)) // find someone who works on the station
target = pick(all_coworkers)
return oldmind
update_explanation_text()
return target
/datum/objective/spendtime //spend some time around someone, handled by the obsessed trauma since that ticks
name = "spendtime"
var/timer = 1800 //5 minutes
var/timer = 0
/datum/objective/spendtime/update_explanation_text()
if(timer == initial(timer))//just so admins can mess with it
timer += pick(-600, 0)
var/datum/antagonist/obsessed/creeper = owner.has_antag_datum(/datum/antagonist/obsessed)
if(target?.current && creeper)
creeper.trauma.attachedobsessedobj = src
if(!timer)
timer = 5 MINUTES + pick(-60 SECONDS, 0)
if(target?.current)
explanation_text = "Spend [DisplayTimeText(timer)] around [target.name] while they're alive."
else
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/spendtime/check_completion()
return timer <= 0 || explanation_text == "Free Objective"
return timer <= 0 || completed
/datum/objective/hug//this objective isn't perfect. hugging the correct amount of times, then switching bodies, might fail the objective anyway. maybe i'll come back and fix this sometime.
name = "hugs"
var/hugs_needed
var/hugs_needed = 0
/datum/objective/hug/update_explanation_text()
..()
if(!hugs_needed)//just so admins can mess with it
if(!hugs_needed)
hugs_needed = rand(4,6)
var/datum/antagonist/obsessed/creeper = owner.has_antag_datum(/datum/antagonist/obsessed)
if(target?.current && creeper)
if(target?.current)
explanation_text = "Hug [target.name] [hugs_needed] times while they're alive."
else
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/hug/check_completion()
var/datum/antagonist/obsessed/creeper = owner.has_antag_datum(/datum/antagonist/obsessed)
if(!creeper || !creeper.trauma || !hugs_needed)
return TRUE//free objective
return creeper.trauma.obsession_hug_count >= hugs_needed
return hugs_needed <= 0 || completed
/datum/objective/polaroid //take a picture of the target with you in it.
name = "polaroid"
/// Weakref to our obsession's original mob
var/datum/weakref/obsession_weakref
/datum/objective/polaroid/update_explanation_text()
..()
if(target?.current)
explanation_text = "Take a photo of [target.name] while they're alive, and keep it in your bag."
else
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/polaroid/check_completion()
var/list/datum/mind/owners = get_owners()
for(var/datum/mind/M in owners)
if(!isliving(M.current))
continue
var/list/all_items = M.current.get_all_contents() //this should get things in cheesewheels, books, etc.
for(var/obj/I in all_items) //Check for wanted items
if(istype(I, /obj/item/photo))
var/obj/item/photo/P = I
if(P.picture && (WEAKREF(target.current) in P.picture.mobs_seen) && !(WEAKREF(target.current) in P.picture.dead_seen)) //Does the picture exist and is the target in it and is the target not dead
return TRUE
for(var/obj/item/photo/photo in owner.current?.get_all_contents())
if((obsession_weakref in photo.picture?.mobs_seen) && !(obsession_weakref in photo.picture?.dead_seen))
return TRUE
return FALSE
@@ -291,11 +279,27 @@
name = "heirloomthief"
/datum/objective/steal/heirloom_thief/update_explanation_text()
..()
if(steal_target)
explanation_text = "Steal [target.name]'s family heirloom, [steal_target] they cherish."
explanation_text = "Steal [target]'s family heirloom, [steal_target] they cherish."
else
explanation_text = "Free Objective"
completed = TRUE
/datum/objective/steal/heirloom_thief/find_target(dupe_search_range, list/blacklist)
for(var/datum/quirk/item_quirk/family_heirloom/quirky in target?.current?.quirks)
steal_target = quirky.heirloom?.resolve()
RegisterSignal(steal_target, COMSIG_QDELETING, PROC_REF(steal_target_deleted))
break
update_explanation_text()
return steal_target
/datum/objective/steal/heirloom_thief/check_completion()
return (!isnull(steal_target) && (steal_target in owner.current?.get_all_contents())) || completed
/datum/objective/steal/heirloom_thief/proc/steal_target_deleted()
SIGNAL_HANDLER
steal_target = null // it's gone, and so are our hopes and dreams
#undef OBSESSED_OBJECTIVE_SPEND_TIME
#undef OBSESSED_OBJECTIVE_POLAROID
+16 -20
View File
@@ -1165,12 +1165,20 @@
/mob/living/resist_grab(moving_resist)
. = TRUE
//Our effective grab state. GRAB_PASSIVE is equal to 0, so if we have no other altering factors to our grab state, we can break free immediately on resist.
var/effective_grab_state = pulledby.grab_state
//The amount of damage inflicted on a failed resist attempt.
var/damage_on_resist_fail = rand(7, 13)
// Base chance to escape a grab. Divided by effective grab state
var/escape_chance = BASE_GRAB_RESIST_CHANCE
var/list/grab_stats = list(
// Our effective grab state.
// GRAB_PASSIVE is equal to 0, so if we have no other altering factors to our grab state, we can break free immediately on resist.
pulledby.grab_state,
/// The amount of damage inflicted on a failed resist attempt.
rand(7, 13),
// Base chance to escape a grab. Divided by effective grab state.
BASE_GRAB_RESIST_CHANCE,
)
SEND_SIGNAL(pulledby, COMSIG_MOVABLE_GRABBED_RESISTING, src, grab_stats)
var/effective_grab_state = grab_stats[GRAB_STAT_EFFECTIVE_STATE]
var/damage_on_resist_fail = grab_stats[GRAB_STAT_FAIL_DAMAGE]
var/escape_chance = grab_stats[GRAB_STAT_ESCAPE_CHANCE]
if(body_position == LYING_DOWN) //If prone, treat the grab state as one higher
effective_grab_state++
@@ -1184,8 +1192,8 @@
if(HAS_TRAIT(src, TRAIT_GRABRESISTANCE)) //If we have grab resistance from some source, treat the grab state as one lower.
effective_grab_state--
//If our puller is a human, and they have an active hand they're grabbing with (please don't ask how people grab without hands), then apply their unarmed values to the grab values
if(pulledby && ishuman(pulledby))
// If our puller is a human, and they have an active hand they're grabbing with (please don't ask how people grab without hands), then apply their unarmed values to the grab values
if(ishuman(pulledby))
var/mob/living/carbon/human/human_puller = pulledby
var/obj/item/bodypart/grabbing_bodypart = human_puller.get_active_hand()
if(grabbing_bodypart)
@@ -1193,18 +1201,6 @@
effective_grab_state += grabbing_bodypart.unarmed_grab_state_bonus
escape_chance += grabbing_bodypart.unarmed_grab_escape_chance_bonus
//If our puller is a drunken brawler, they add more damage based on their own damage taken so long as they're drunk and treat the grab state as one higher
var/puller_drunkenness = human_puller.get_drunk_amount()
if(puller_drunkenness && HAS_TRAIT(human_puller, TRAIT_DRUNKEN_BRAWLER))
damage_on_resist_fail += clamp((human_puller.get_fire_loss() + human_puller.get_brute_loss()) / 10, 3, 20)
effective_grab_state++
var/datum/martial_art/puller_art = GET_ACTIVE_MARTIAL_ART(human_puller)
if(puller_art?.can_use(human_puller))
damage_on_resist_fail += puller_art.grab_damage_modifier
effective_grab_state += puller_art.grab_state_modifier
escape_chance += puller_art.grab_escape_chance_modifier
//We only resist our grab state if we are currently in a grab equal to or greater than GRAB_AGGRESSIVE (1). Otherwise, break out immediately!
if(effective_grab_state >= GRAB_AGGRESSIVE)
// see defines/combat.dm, this should be baseline 60%