diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index 412f5701eeb..39e4ee1e0ba 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -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() diff --git a/code/datums/brain_damage/creepy_trauma.dm b/code/datums/brain_damage/creepy_trauma.dm index 8cc32018531..d94be273c89 100644 --- a/code/datums/brain_damage/creepy_trauma.dm +++ b/code/datums/brain_damage/creepy_trauma.dm @@ -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 diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm index 53dbaae321d..acab041be5d 100644 --- a/code/datums/martial/_martial.dm +++ b/code/datums/martial/_martial.dm @@ -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. diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm index 6422ee8c3d1..d277c3cffad 100644 --- a/code/datums/mind/antag.dm +++ b/code/datums/mind/antag.dm @@ -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) diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm index 6c16aed83b9..e72a6446bfe 100644 --- a/code/datums/mood_events/generic_positive_events.dm +++ b/code/datums/mood_events/generic_positive_events.dm @@ -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 diff --git a/code/datums/status_effects/debuffs/drunk.dm b/code/datums/status_effects/debuffs/drunk.dm index 36860ae0a6b..dcfff81332a 100644 --- a/code/datums/status_effects/debuffs/drunk.dm +++ b/code/datums/status_effects/debuffs/drunk.dm @@ -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)) diff --git a/code/datums/status_effects/debuffs/speech_debuffs.dm b/code/datums/status_effects/debuffs/speech_debuffs.dm index adb82b99023..87bd2e6a939 100644 --- a/code/datums/status_effects/debuffs/speech_debuffs.dm +++ b/code/datums/status_effects/debuffs/speech_debuffs.dm @@ -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 diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index eaca9403857..47f82bbc4ca 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -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 diff --git a/code/modules/antagonists/obsessed/obsessed.dm b/code/modules/antagonists/obsessed/obsessed.dm index 1cd433c055e..77494d60d24 100644 --- a/code/modules/antagonists/obsessed/obsessed.dm +++ b/code/modules/antagonists/obsessed/obsessed.dm @@ -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 diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index cb665832ec4..77e3bb72118 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -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%