diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index 040930e5205..03b8514e0cf 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -67,12 +67,27 @@ #define EFFECT_PARALYZE "paralyze" #define EFFECT_IMMOBILIZE "immobilize" //Bitflags defining which status effects could be or are inflicted on a mob +/// If set, this mob can be stunned. #define CANSTUN (1<<0) +/// If set, this mob can be knocked down (or stamcrit) #define CANKNOCKDOWN (1<<1) +/// If set, this mob can be knocked unconscious via status effect. +/// NOTE, does not mean immune to sleep. Unconscious and sleep are two different things. +/// NOTE, does not relate to the unconscious stat either. Only the status effect. #define CANUNCONSCIOUS (1<<2) +/// If set, this mob can be grabbed or pushed when bumped into #define CANPUSH (1<<3) +/// Mob godmode. Prevents most statuses and damage from being taken, but is more often than not a crapshoot. Use with caution. #define GODMODE (1<<4) +DEFINE_BITFIELD(status_flags, list( + "CAN STUN" = CANSTUN, + "CAN KNOCKDOWN" = CANKNOCKDOWN, + "CAN UNCONSCIOUS" = CANUNCONSCIOUS, + "CAN PUSH" = CANPUSH, + "GOD MODE" = GODMODE, +)) + //Health Defines #define HEALTH_THRESHOLD_CRIT 0 #define HEALTH_THRESHOLD_FULLCRIT -30 diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index 9ad327ca4ff..ee227f8deca 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -80,10 +80,14 @@ #define COMSIG_LIVING_STATUS_PARALYZE "living_paralyze" ///from base of mob/living/Immobilize() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_IMMOBILIZE "living_immobilize" +///from base of mob/living/incapacitate() (amount, ignore_canstun) +#define COMSIG_LIVING_STATUS_INCAPACITATE "living_incapacitate" ///from base of mob/living/Unconscious() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_UNCONSCIOUS "living_unconscious" ///from base of mob/living/Sleeping() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_SLEEP "living_sleeping" +/// from mob/living/check_stun_immunity(): (check_flags) +#define COMSIG_LIVING_GENERIC_STUN_CHECK "living_check_stun" #define COMPONENT_NO_STUN (1<<0) //For all of them ///from base of /mob/living/can_track(): (mob/user) #define COMSIG_LIVING_CAN_TRACK "mob_cantrack" diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 7261cacc66f..1972f4d2f53 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -184,6 +184,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_DISFIGURED "disfigured" /// Tracks whether we're gonna be a baby alien's mummy. #define TRAIT_XENO_HOST "xeno_host" +/// This mob is immune to stun causing status effects and stamcrit. +/// Prefer to use [/mob/living/proc/check_stun_immunity] over checking for this trait exactly. #define TRAIT_STUNIMMUNE "stun_immunity" #define TRAIT_BATON_RESISTANCE "baton_resistance" /// Anti Dual-baton cooldown bypass exploit. @@ -863,7 +865,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define CHANGELING_DRAIN "drain" /// changelings with this trait can no longer talk over the hivemind #define CHANGELING_HIVEMIND_MUTE "ling_mute" -#define HIGHLANDER "highlander" #define TRAIT_HULK "hulk" #define STASIS_MUTE "stasis" #define GENETICS_SPELL "genetics_spell" diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index 3d34b76c14f..62dd6ca4b46 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -22,10 +22,16 @@ return ..() /datum/status_effect/his_grace/on_apply() - owner.log_message("gained His Grace's stun immunity", LOG_ATTACK) - owner.add_stun_absorption("hisgrace", INFINITY, 3, null, "His Grace protects you from the stun!") + owner.add_stun_absorption( + source = id, + priority = 3, + self_message = span_boldwarning("His Grace protects you from the stun!"), + ) return ..() +/datum/status_effect/his_grace/on_remove() + owner.remove_stun_absorption(id) + /datum/status_effect/his_grace/tick() bloodlust = 0 var/graces = 0 @@ -45,11 +51,6 @@ owner.adjustOxyLoss(-(grace_heal * 2)) owner.adjustCloneLoss(-grace_heal) -/datum/status_effect/his_grace/on_remove() - owner.log_message("lost His Grace's stun immunity", LOG_ATTACK) - if(islist(owner.stun_absorption) && owner.stun_absorption["hisgrace"]) - owner.stun_absorption -= "hisgrace" - /datum/status_effect/wish_granters_gift //Fully revives after ten seconds. id = "wish_granters_gift" @@ -112,34 +113,30 @@ icon_state = "blooddrunk" /datum/status_effect/blooddrunk/on_apply() - . = ..() - if(.) - ADD_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) - if(ishuman(owner)) - var/mob/living/carbon/human/H = owner - H.physiology.brute_mod *= 0.1 - H.physiology.burn_mod *= 0.1 - H.physiology.tox_mod *= 0.1 - H.physiology.oxy_mod *= 0.1 - H.physiology.clone_mod *= 0.1 - H.physiology.stamina_mod *= 0.1 - owner.log_message("gained blood-drunk stun immunity", LOG_ATTACK) - owner.add_stun_absorption("blooddrunk", INFINITY, 4) - owner.playsound_local(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1, use_reverb = FALSE) + ADD_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) + if(ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + human_owner.physiology.brute_mod *= 0.1 + human_owner.physiology.burn_mod *= 0.1 + human_owner.physiology.tox_mod *= 0.1 + human_owner.physiology.oxy_mod *= 0.1 + human_owner.physiology.clone_mod *= 0.1 + human_owner.physiology.stamina_mod *= 0.1 + owner.add_stun_absorption(source = id, priority = 4) + owner.playsound_local(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1, use_reverb = FALSE) + return TRUE /datum/status_effect/blooddrunk/on_remove() if(ishuman(owner)) - var/mob/living/carbon/human/H = owner - H.physiology.brute_mod *= 10 - H.physiology.burn_mod *= 10 - H.physiology.tox_mod *= 10 - H.physiology.oxy_mod *= 10 - H.physiology.clone_mod *= 10 - H.physiology.stamina_mod *= 10 - owner.log_message("lost blood-drunk stun immunity", LOG_ATTACK) - REMOVE_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT); - if(islist(owner.stun_absorption) && owner.stun_absorption["blooddrunk"]) - owner.stun_absorption -= "blooddrunk" + var/mob/living/carbon/human/human_owner = owner + human_owner.physiology.brute_mod *= 10 + human_owner.physiology.burn_mod *= 10 + human_owner.physiology.tox_mod *= 10 + human_owner.physiology.oxy_mod *= 10 + human_owner.physiology.clone_mod *= 10 + human_owner.physiology.stamina_mod *= 10 + REMOVE_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) + owner.remove_stun_absorption(id) //Used by changelings to rapidly heal //Heals 10 brute and oxygen damage every second, and 5 fire diff --git a/code/datums/status_effects/buffs/stun_absorption.dm b/code/datums/status_effects/buffs/stun_absorption.dm new file mode 100644 index 00000000000..d68f2f7408c --- /dev/null +++ b/code/datums/status_effects/buffs/stun_absorption.dm @@ -0,0 +1,235 @@ +/** + * # Stun absorption + * + * A status effect effectively functions as [TRAIT_STUNIMMUNE], but with additional effects tied to it, + * such as showing a message on trigger / examine, or only blocking a limited amount of stuns. + * + * Apply this via [/mob/living/proc/add_stun_absorption]. If you do not supply a duration, + * remove this via [/mob/living/proc/remove_stun_absorption]. + */ +/datum/status_effect/stun_absorption + id = "absorb_stun" + tick_interval = -1 + alert_type = null + status_type = STATUS_EFFECT_MULTIPLE + + /// The string key sourcer of the stun absorption, used for logging + var/source + /// The priority of the stun absorption. Used so that multiple sources will not trigger at once. + /// This number is arbitrary but try to keep in sane / in line with other sources that exist. + var/priority = -1 + /// How many total seconds of stuns that have been blocked. + var/seconds_of_stuns_absorbed = 0 SECONDS + /// The max number of seconds we can block before self-deleting. + var/max_seconds_of_stuns_blocked = INFINITY + /// The message shown via visible message to all nearby mobs when the effect triggers. + var/shown_message + /// The message shown to the owner when the effect triggers. + var/self_message + /// Message shown on anyone examining the owner. + var/examine_message + + /// Static list of all generic "stun received " signals that we will react to and block. + /// These all have the same arguments sent, so we can handle them all via the same signal handler. + /// Note though that we can register other signals to block effects outside of these if we want. + var/static/list/incapacitation_effect_signals = list( + COMSIG_LIVING_STATUS_IMMOBILIZE, + COMSIG_LIVING_STATUS_INCAPACITATE, + COMSIG_LIVING_STATUS_KNOCKDOWN, + COMSIG_LIVING_STATUS_PARALYZE, + COMSIG_LIVING_STATUS_STUN, + ) + +/datum/status_effect/stun_absorption/on_creation( + mob/living/new_owner, + source, + duration, + priority = -1, + shown_message, + self_message, + examine_message, + max_seconds_of_stuns_blocked = INFINITY, +) + + if(isnum(duration)) + src.duration = duration + + src.source = source + src.priority = priority + src.shown_message = shown_message + src.self_message = self_message + src.examine_message = examine_message + src.max_seconds_of_stuns_blocked = max_seconds_of_stuns_blocked + + return ..() + +/datum/status_effect/stun_absorption/on_apply() + if(owner.mind || owner.client) + owner.log_message("gained stun absorption (from: [source || "Unknown"])", LOG_ATTACK) + + RegisterSignals(owner, incapacitation_effect_signals, PROC_REF(try_absorb_incapacitating_effect)) + RegisterSignal(owner, COMSIG_LIVING_GENERIC_STUN_CHECK, PROC_REF(try_absorb_generic_effect)) + return TRUE + +/datum/status_effect/stun_absorption/on_remove() + if(owner.mind || owner.client) + owner.log_message("lost stun absorption (from: [source || "Unknown"])", LOG_ATTACK) + + UnregisterSignal(owner, incapacitation_effect_signals) + UnregisterSignal(owner, COMSIG_LIVING_GENERIC_STUN_CHECK) + +/datum/status_effect/stun_absorption/get_examine_text() + return replacetext(examine_message, "%EFFECT_OWNER_THEYRE", owner.p_theyre(TRUE)) + +/** + * Signal proc for generic stun signals being sent, such as [COMSIG_LIVING_STATUS_STUN] or [COMSIG_LIVING_STATUS_KNOCKDOWN]. + * + * When we get stunned, we will try to absorb a number of seconds from the stun, and return [COMPONENT_NO_STUN] if we succeed. + */ +/datum/status_effect/stun_absorption/proc/try_absorb_incapacitating_effect(mob/living/source, amount = 0, ignore_canstun = FALSE) + SIGNAL_HANDLER + + // we blocked a stun this tick that resulting is us qdeling, so stop + if(QDELING(src)) + return NONE + + // Amount less than (or equal to) zero is removing stuns, so we don't want to block that + if(amount <= 0 || ignore_canstun) + return NONE + + if(!absorb_stun(amount)) + return NONE + + return COMPONENT_NO_STUN + +/** + * Signal proc for [COMSIG_LIVING_GENERIC_STUN_CHECK]. (Note, this includes being stamcrit) + * + * Whenever a generic stun check is done against us, we'll just try to block it with "0 second" stun. + * This prevents spam us from showing feedback messages, and is for the generic "can be stunned" check. + */ +/datum/status_effect/stun_absorption/proc/try_absorb_generic_effect(mob/living/source, check_flags) + SIGNAL_HANDLER + + if(QDELING(src)) + return NONE + + // "0 amount" / "0 seconds of stun" is used so no feedback is sent on success + if(!absorb_stun(0)) + return NONE + + return COMPONENT_NO_STUN + +/** + * Absorb a number of seconds of stuns. + * If we hit the max amount of absorption, we will qdel ourself in this proc. + * + * * amount - this is the number of deciseconds being absorbed at once. + * + * Returns TRUE on successful absorption, or FALSE otherwise. + */ +/datum/status_effect/stun_absorption/proc/absorb_stun(amount) + if(owner.stat != CONSCIOUS) + return FALSE + + // Now we gotta check that no other stun absorption we have is blocking us + for(var/datum/status_effect/stun_absorption/similar_effect in owner.status_effects) + if(similar_effect == src) + continue + // they blocked a stun this tick that resulted in them qdeling, so disregard + if(QDELING(similar_effect)) + continue + // if we have another stun absorption with higher priority, + // don't do anything, let them handle it instead + if(similar_effect.priority > priority) + return FALSE + + // At this point, a stun was successfully absorbed + + // Only do effects if the amount was > 0 seconds + if(amount > 0 SECONDS) + // Show the message + if(shown_message) + // We do this replacement meme, instead of just setting it up in creation, + // so that we respect indentity changes done while active + var/really_shown_message = replacetext(shown_message, "%EFFECT_OWNER", "[owner]") + owner.visible_message(really_shown_message, ignored_mobs = owner) + + // Send the self message + if(self_message) + to_chat(owner, self_message) + + // Count seconds absorbed + seconds_of_stuns_absorbed += amount + if(seconds_of_stuns_absorbed >= max_seconds_of_stuns_blocked) + qdel(src) + + return TRUE + +/** + * [proc/apply_status_effect] wrapper specifically for [/datum/status_effect/stun_absorption], + * specifically so that it's easier to apply stun absorptions with named arguments. + * + * If the mob already has a stun absorption from the same source, will not re-apply the effect, + * unless the new effect's priority is higher than the old effect's priority. + * + * Arguments + * * source - the source of the stun absorption. + * * duration - how long does the stun absorption last before it ends? -1 or null = infinite duration + * * priority - what is this effect's priority to other stun absorptions? higher = more priority + * * message - optional, "other message" arg of visible message, shown on trigger. Use %EFFECT_OWNER if you want the owner's name to be inserted. + * * self_message - optional, "self message" arg of visible message, shown on trigger + * * examine_message - optional, what is shown on examine of the mob. + * * max_seconds_of_stuns_blocked - optional, how many seconds of stuns can it block before deleting? the stun that breaks over this number is still blocked, even if it is much higher. + * + * Returns an instance of a stun absorption effect, or NULL if failure + */ +/mob/living/proc/add_stun_absorption( + source, + duration, + priority = -1, + message, + self_message, + examine_message, + max_seconds_of_stuns_blocked = INFINITY, +) + + // Handle duplicate sources + for(var/datum/status_effect/stun_absorption/existing_effect in status_effects) + if(existing_effect.source != source) + continue + + // If an existing effect's priority is greater or equal to our passed priority... + if(existing_effect.priority >= priority) + // don't bother re-applying the effect, and return + return + + // otherwise, delete existing and replace with new + qdel(existing_effect) + + return apply_status_effect( + /datum/status_effect/stun_absorption, + source, + duration, + priority, + message, + self_message, + examine_message, + max_seconds_of_stuns_blocked, + ) + +/** + * Removes all stub absorptions with the passed source. + * + * Returns TRUE if an effect was deleted, FALSE otherwise + */ +/mob/living/proc/remove_stun_absorption(source) + . = FALSE + for(var/datum/status_effect/stun_absorption/effect in status_effects) + if(effect.source != source) + continue + + qdel(effect) + . = TRUE + + return . diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index 80086f96725..3eda40d51ab 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -151,12 +151,18 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 /obj/item/claymore/highlander/pickup(mob/living/user) . = ..() to_chat(user, span_notice("The power of Scotland protects you! You are shielded from all stuns and knockdowns.")) - user.add_stun_absorption("highlander", INFINITY, 1, " is protected by the power of Scotland!", "The power of Scotland absorbs the stun!", " is protected by the power of Scotland!") - user.ignore_slowdown(HIGHLANDER) + user.ignore_slowdown(HIGHLANDER_TRAIT) + user.add_stun_absorption( + source = HIGHLANDER_TRAIT, + message = span_warning("%EFFECT_OWNER is protected by the power of Scotland!"), + self_message = span_boldwarning("The power of Scotland absorbs the stun!"), + examine_message = span_warning("%EFFECT_OWNER_THEYRE protected by the power of Scotland!"), + ) /obj/item/claymore/highlander/dropped(mob/living/user) . = ..() - user.unignore_slowdown(HIGHLANDER) + user.unignore_slowdown(HIGHLANDER_TRAIT) + user.remove_stun_absorption(HIGHLANDER_TRAIT) /obj/item/claymore/highlander/examine(mob/user) . = ..() diff --git a/code/modules/antagonists/cult/cult_bastard_sword.dm b/code/modules/antagonists/cult/cult_bastard_sword.dm index 18b14f4b2fc..1d1179787a5 100644 --- a/code/modules/antagonists/cult/cult_bastard_sword.dm +++ b/code/modules/antagonists/cult/cult_bastard_sword.dm @@ -46,7 +46,14 @@ /obj/item/cult_bastard/proc/on_spin(mob/living/user, duration) var/oldcolor = user.color user.color = "#ff0000" - user.add_stun_absorption("bloody bastard sword", duration, 2, "doesn't even flinch as the sword's power courses through them!", "You shrug off the stun!", " glowing with a blazing red aura!") + user.add_stun_absorption( + source = name, + duration = duration, + priority = 2, + message = span_warning("%EFFECT_OWNER doesn't even flinch as the sword's power courses through [user.p_them()]!"), + self_message = span_boldwarning("You shrug off the stun!"), + examine_message = span_warning("%EFFECT_OWNER_THEYRE glowing with a blazing red aura!"), + ) user.spin(duration, 1) animate(user, color = oldcolor, time = duration, easing = EASE_IN) addtimer(CALLBACK(user, TYPE_PROC_REF(/atom, update_atom_colour)), duration) diff --git a/code/modules/antagonists/highlander/highlander.dm b/code/modules/antagonists/highlander/highlander.dm index c740de9279f..98659ef1941 100644 --- a/code/modules/antagonists/highlander/highlander.dm +++ b/code/modules/antagonists/highlander/highlander.dm @@ -66,7 +66,7 @@ P.attack_self(H) var/obj/item/card/id/advanced/highlander/W = new(H) W.registered_name = H.real_name - ADD_TRAIT(W, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(W, TRAIT_NODROP, HIGHLANDER_TRAIT) W.update_label() W.update_icon() H.equip_to_slot_or_del(W, ITEM_SLOT_ID) diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm index 24387971d15..224e7642686 100644 --- a/code/modules/clothing/head/jobs.dm +++ b/code/modules/clothing/head/jobs.dm @@ -463,7 +463,7 @@ /obj/item/clothing/head/beret/highlander/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER_TRAIT) //CentCom /obj/item/clothing/head/beret/centcom_formal diff --git a/code/modules/clothing/under/costume.dm b/code/modules/clothing/under/costume.dm index fd7db5abb12..ef1f0002c1b 100644 --- a/code/modules/clothing/under/costume.dm +++ b/code/modules/clothing/under/costume.dm @@ -90,7 +90,7 @@ /obj/item/clothing/under/costume/kilt/highlander/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER_TRAIT) /obj/item/clothing/under/costume/gladiator name = "gladiator uniform" diff --git a/code/modules/hallucination/stray_bullet.dm b/code/modules/hallucination/stray_bullet.dm index b7fe2814713..97f75f95061 100644 --- a/code/modules/hallucination/stray_bullet.dm +++ b/code/modules/hallucination/stray_bullet.dm @@ -242,7 +242,7 @@ ";AAAAAAARRRGH!"), forced = "hulk (hallucinating)", ) - else if((afflicted.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(afflicted, TRAIT_STUNIMMUNE)) + else if(!afflicted.check_stun_immunity(CANKNOCKDOWN)) addtimer(CALLBACK(afflicted, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 0.5 SECONDS) /obj/projectile/hallucination/disabler diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 6b4df5b3697..1a2a2ba7e4e 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -291,11 +291,6 @@ if(reagents.has_reagent(/datum/reagent/teslium, needs_metabolizing = TRUE)) msg += "[t_He] [t_is] emitting a gentle blue glow!\n" - if(islist(stun_absorption)) - for(var/i in stun_absorption) - if(stun_absorption[i]["end_time"] > world.time && stun_absorption[i]["examine_message"]) - msg += "[t_He] [t_is][stun_absorption[i]["examine_message"]]\n" - if(just_sleeping) msg += "[t_He] [t_is]n't responding to anything around [t_him] and seem[p_s()] to be asleep.\n" diff --git a/code/modules/mob/living/carbon/status_procs.dm b/code/modules/mob/living/carbon/status_procs.dm index 0dbd9443177..2329506fc1f 100644 --- a/code/modules/mob/living/carbon/status_procs.dm +++ b/code/modules/mob/living/carbon/status_procs.dm @@ -6,12 +6,11 @@ return ..() || (include_stamcrit && HAS_TRAIT_FROM(src, TRAIT_INCAPACITATED, STAMINA)) /mob/living/carbon/proc/enter_stamcrit() - if(!(status_flags & CANKNOCKDOWN) || HAS_TRAIT(src, TRAIT_STUNIMMUNE)) - return if(HAS_TRAIT_FROM(src, TRAIT_INCAPACITATED, STAMINA)) //Already in stamcrit return - if(absorb_stun(0)) //continuous effect, so we don't want it to increment the stuns absorbed. + if(check_stun_immunity(CANKNOCKDOWN)) return + to_chat(src, span_notice("You're too exhausted to keep going...")) add_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_FLOORED), STAMINA) if(getStaminaLoss() < 120) // Puts you a little further into the initial stamcrit, makes stamcrit harder to outright counter with chems. diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index a46017fd296..faefa04b61f 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -150,7 +150,7 @@ adjust_drowsiness(drowsy) if(eyeblur) adjust_eye_blur(eyeblur) - if(jitter && (status_flags & CANSTUN) && !HAS_TRAIT(src, TRAIT_STUNIMMUNE)) + if(jitter && !check_stun_immunity(CANSTUN)) adjust_jitter(jitter) if(slur) adjust_slurring(slur) diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm index d1c2a1da80f..deab04e0d8c 100644 --- a/code/modules/mob/living/silicon/robot/robot_model.dm +++ b/code/modules/mob/living/silicon/robot/robot_model.dm @@ -941,7 +941,7 @@ robot.equip_module_to_slot(locate(/obj/item/claymore/highlander/robot) in basic_modules, 1) robot.equip_module_to_slot(locate(/obj/item/pinpointer/nuke) in basic_modules, 2) robot.place_on_head(new /obj/item/clothing/head/beret/highlander(robot)) //THE ONLY PART MORE IMPORTANT THAN THE SWORD IS THE HAT - ADD_TRAIT(robot.hat, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(robot.hat, TRAIT_NODROP, HIGHLANDER_TRAIT) // ------------------------------------------ Storages diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm index c401c51ae8b..d7214a4de7b 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm @@ -97,9 +97,8 @@ Difficulty: Medium open_force = 10 /obj/item/melee/cleaving_saw/miner/attack(mob/living/target, mob/living/carbon/human/user) - target.add_stun_absorption("miner", 10, INFINITY) - . = ..() - target.stun_absorption -= "miner" + target.add_stun_absorption(source = "miner", duration = 1 SECONDS, priority = INFINITY) + return ..() /obj/projectile/kinetic/miner damage = 20 diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm index c7831346cd6..accbf9de890 100644 --- a/code/modules/mob/living/status_procs.dm +++ b/code/modules/mob/living/status_procs.dm @@ -1,7 +1,33 @@ -//Here are the procs used to modify status effects of a mob. -//The effects include: stun, knockdown, unconscious, sleeping, resting -#define IS_STUN_IMMUNE(source, ignore_canstun) ((source.status_flags & GODMODE) || (!ignore_canstun && (!(source.status_flags & CANKNOCKDOWN) || HAS_TRAIT(source, TRAIT_STUNIMMUNE)))) +/** + * Checks if we have stun immunity. Godmode always passes this check. + * + * * check_flags - bitflag of status flags that must be set in order for the stun to succeed. Passing NONE will always return false. + * * force_stun - whether we ignore stun immunity with the exception of godmode + * + * returns TRUE if stun immune, FALSE otherwise + */ +/mob/living/proc/check_stun_immunity(check_flags = CANSTUN, force_stun = FALSE) + SHOULD_CALL_PARENT(TRUE) + + if(status_flags & GODMODE) + return TRUE + + if(force_stun) // Does not take priority over god mode? I guess + return FALSE + + if(SEND_SIGNAL(src, COMSIG_LIVING_GENERIC_STUN_CHECK, check_flags, force_stun) & COMPONENT_NO_STUN) + return TRUE + + if(HAS_TRAIT(src, TRAIT_STUNIMMUNE)) + return TRUE + + // Do we have the correct flag set to allow this status? + // This checks that ALL flags are set, not just one of them. + if((status_flags & check_flags) == check_flags) + return FALSE + + return TRUE /* STUN */ /mob/living/proc/IsStun() //If we're stunned @@ -16,9 +42,7 @@ /mob/living/proc/Stun(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(S) @@ -30,15 +54,13 @@ /mob/living/proc/SetStun(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(amount <= 0) if(S) qdel(S) else - if(absorb_stun(amount, ignore_canstun)) - return if(S) S.duration = world.time + amount else @@ -48,9 +70,7 @@ /mob/living/proc/AdjustStun(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(S) @@ -72,9 +92,7 @@ /mob/living/proc/Knockdown(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(K) @@ -86,15 +104,13 @@ /mob/living/proc/SetKnockdown(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(amount <= 0) if(K) qdel(K) else - if(absorb_stun(amount, ignore_canstun)) - return if(K) K.duration = world.time + amount else @@ -104,9 +120,7 @@ /mob/living/proc/AdjustKnockdown(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(K) @@ -128,9 +142,7 @@ /mob/living/proc/Immobilize(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(I) @@ -142,15 +154,13 @@ /mob/living/proc/SetImmobilized(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(amount <= 0) if(I) qdel(I) else - if(absorb_stun(amount, ignore_canstun)) - return if(I) I.duration = world.time + amount else @@ -160,9 +170,7 @@ /mob/living/proc/AdjustImmobilized(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(I) @@ -184,9 +192,7 @@ /mob/living/proc/Paralyze(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) // this requires both can stun and can knockdown return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(P) @@ -198,15 +204,13 @@ /mob/living/proc/SetParalyzed(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(amount <= 0) if(P) qdel(P) else - if(absorb_stun(amount, ignore_canstun)) - return if(P) P.duration = world.time + amount else @@ -216,9 +220,7 @@ /mob/living/proc/AdjustParalyzed(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(P) @@ -244,9 +246,9 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/incapacitate(amount, ignore_canstun = FALSE) - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(incapacitated_status_effect) @@ -262,15 +264,15 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/set_incapacitated(amount, ignore_canstun = FALSE) - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) + return + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(amount <= 0) if(incapacitated_status_effect) qdel(incapacitated_status_effect) else - if(absorb_stun(amount, ignore_canstun)) - return if(incapacitated_status_effect) incapacitated_status_effect.duration = world.time + amount else @@ -284,9 +286,9 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/adjust_incapacitated(amount, ignore_canstun = FALSE) //Adds to remaining duration - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(incapacitated_status_effect) @@ -330,7 +332,7 @@ /mob/living/proc/Unconscious(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(U) @@ -342,7 +344,7 @@ /mob/living/proc/SetUnconscious(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(amount <= 0) @@ -357,7 +359,7 @@ /mob/living/proc/AdjustUnconscious(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(U) @@ -445,49 +447,11 @@ /mob/living/proc/IsFrozen() return has_status_effect(/datum/status_effect/freon) - -/* STUN ABSORPTION*/ -/mob/living/proc/add_stun_absorption(key, duration, priority, message, self_message, examine_message) -//adds a stun absorption with a key, a duration in deciseconds, its priority, and the messages it makes when you're stun/examined, if any - if(!islist(stun_absorption)) - stun_absorption = list() - if(stun_absorption[key]) - stun_absorption[key]["end_time"] = world.time + duration - stun_absorption[key]["priority"] = priority - stun_absorption[key]["stuns_absorbed"] = 0 - else - stun_absorption[key] = list("end_time" = world.time + duration, "priority" = priority, "stuns_absorbed" = 0, \ - "visible_message" = message, "self_message" = self_message, "examine_message" = examine_message) - -/mob/living/proc/absorb_stun(amount, ignoring_flag_presence) - if(amount < 0 || stat || ignoring_flag_presence || !islist(stun_absorption)) - return FALSE - if(!amount) - amount = 0 - var/priority_absorb_key - var/highest_priority - for(var/i in stun_absorption) - if(stun_absorption[i]["end_time"] > world.time && (!priority_absorb_key || stun_absorption[i]["priority"] > highest_priority)) - priority_absorb_key = stun_absorption[i] - highest_priority = priority_absorb_key["priority"] - if(priority_absorb_key) - if(amount) //don't spam up the chat for continuous stuns - if(priority_absorb_key["visible_message"] || priority_absorb_key["self_message"]) - if(priority_absorb_key["visible_message"] && priority_absorb_key["self_message"]) - visible_message(span_warning("[src][priority_absorb_key["visible_message"]]"), span_boldwarning("[priority_absorb_key["self_message"]]")) - else if(priority_absorb_key["visible_message"]) - visible_message(span_warning("[src][priority_absorb_key["visible_message"]]")) - else if(priority_absorb_key["self_message"]) - to_chat(src, span_boldwarning("[priority_absorb_key["self_message"]]")) - priority_absorb_key["stuns_absorbed"] += amount - return TRUE - /** * Adds the passed quirk to the mob * * Arguments * * quirktype - Quirk typepath to add to the mob - * * override_client - optional, allows a client to be passed to the quirks on add procs. * If not passed, defaults to this mob's client. * * Returns TRUE on success, FALSE on failure (already has the quirk, etc) @@ -759,5 +723,3 @@ /// Helper to check if we seem to be alive or not /mob/living/proc/appears_alive() return health >= 0 && !HAS_TRAIT(src, TRAIT_FAKEDEATH) - -#undef IS_STUN_IMMUNE diff --git a/code/modules/projectiles/projectile/energy/stun.dm b/code/modules/projectiles/projectile/energy/stun.dm index 261dab29b27..03cf5f85d84 100644 --- a/code/modules/projectiles/projectile/energy/stun.dm +++ b/code/modules/projectiles/projectile/energy/stun.dm @@ -21,7 +21,7 @@ SEND_SIGNAL(C, COMSIG_LIVING_MINOR_SHOCK) if(C.dna && C.dna.check_mutation(/datum/mutation/human/hulk)) C.say(pick(";RAAAAAAAARGH!", ";HNNNNNNNNNGGGGGGH!", ";GWAAAAAAAARRRHHH!", "NNNNNNNNGGGGGGGGHH!", ";AAAAAAARRRGH!" ), forced = "hulk") - else if((C.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(C, TRAIT_STUNIMMUNE)) + else if(!C.check_stun_immunity(CANKNOCKDOWN)) addtimer(CALLBACK(C, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 5) /obj/projectile/energy/electrode/on_range() //to ensure the bolt sparks when it reaches the end of its range if it didn't hit a target yet diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index d3a5aafdf7e..6d32ebf33cc 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -223,6 +223,7 @@ #include "stomach.dm" #include "strange_reagent.dm" #include "strippable.dm" +#include "stuns.dm" #include "subsystem_init.dm" #include "suit_storage_icons.dm" #include "surgeries.dm" diff --git a/code/modules/unit_tests/stuns.dm b/code/modules/unit_tests/stuns.dm new file mode 100644 index 00000000000..68110e72e55 --- /dev/null +++ b/code/modules/unit_tests/stuns.dm @@ -0,0 +1,72 @@ +/// Tests stun and the canstun flag +/datum/unit_test/stun + +/datum/unit_test/stun/Run() + var/mob/living/carbon/human/gets_stunned = allocate(/mob/living/carbon/human/consistent) + + gets_stunned.Stun(1 SECONDS) + TEST_ASSERT(gets_stunned.IsStun(), "Stun() failed to apply stun") + + gets_stunned.SetStun(0 SECONDS) + TEST_ASSERT(!gets_stunned.IsStun(), "SetStun(0) failed to clear stun") + + gets_stunned.status_flags &= ~CANSTUN + gets_stunned.Stun(1 SECONDS) + TEST_ASSERT(!gets_stunned.IsStun(), "Stun() stunned despite not having CANSTUN flag") + +/// Tests knockdown and the canknockdown flag +/datum/unit_test/knockdown + +/datum/unit_test/knockdown/Run() + var/mob/living/carbon/human/gets_knockdown = allocate(/mob/living/carbon/human/consistent) + + gets_knockdown.Knockdown(1 SECONDS) + TEST_ASSERT(gets_knockdown.IsKnockdown(), "Knockdown() failed to apply knockdown") + + gets_knockdown.SetKnockdown(0 SECONDS) + TEST_ASSERT(!gets_knockdown.IsKnockdown(), "SetKnockdown(0) failed to clear knockdown") + + gets_knockdown.status_flags &= ~CANKNOCKDOWN + gets_knockdown.Knockdown(1 SECONDS) + TEST_ASSERT(!gets_knockdown.IsKnockdown(), "Knockdown() knocked over despite not having CANKNOCKDOWN flag") + +/// Tests paralyze and stuns that have two flags checked (in this case, canstun and canknockdown) +/datum/unit_test/paralyze + +/datum/unit_test/paralyze/Run() + var/mob/living/carbon/human/gets_paralyzed = allocate(/mob/living/carbon/human/consistent) + + gets_paralyzed.Paralyze(1 SECONDS) + TEST_ASSERT(gets_paralyzed.IsParalyzed(), "Paralyze() failed to apply paralyze") + + gets_paralyzed.SetParalyzed(0 SECONDS) + TEST_ASSERT(!gets_paralyzed.IsParalyzed(), "SetParalyzed(0) failed to clear paralyze") + + gets_paralyzed.status_flags &= ~CANSTUN // paralyze needs both CANSTUN and CANKNOCKDOWN to succeed + gets_paralyzed.Paralyze(1 SECONDS) + TEST_ASSERT(!gets_paralyzed.IsParalyzed(), "Paralyze() paralyzed a mob despite not having CANSTUN flag (but still having CANKNOCKDOWN)") + +/// Tests unconsciousness and the canunconscious flag +/datum/unit_test/unconsciousness + +/datum/unit_test/unconsciousness/Run() + var/mob/living/carbon/human/gets_unconscious = allocate(/mob/living/carbon/human/consistent) + + gets_unconscious.Unconscious(1 SECONDS) + TEST_ASSERT(gets_unconscious.IsUnconscious(), "Unconscious() failed to apply unconsciousness") + + gets_unconscious.SetUnconscious(0 SECONDS) + TEST_ASSERT(!gets_unconscious.IsUnconscious(), "SetUnconscious(0) failed to clear unconsciousness") + + gets_unconscious.status_flags &= ~CANUNCONSCIOUS + gets_unconscious.Unconscious(1 SECONDS) + TEST_ASSERT(!gets_unconscious.IsUnconscious(), "Unconscious() knocked unconscious despite not having CANUNCONSCIOUS flag") + +/// Tests for stun absorption +/datum/unit_test/stun_absorb + +/datum/unit_test/stun_absorb/Run() + var/mob/living/carbon/human/doesnt_get_stunned = allocate(/mob/living/carbon/human/consistent) + doesnt_get_stunned.add_stun_absorption(source = TRAIT_SOURCE_UNIT_TESTS) + doesnt_get_stunned.Stun(1 SECONDS) + TEST_ASSERT(!doesnt_get_stunned.IsStun(), "Stun() stunned despite having stun absorption") diff --git a/tgstation.dme b/tgstation.dme index fb2c94238ef..300c38becae 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1421,6 +1421,7 @@ #include "code\datums\status_effects\song_effects.dm" #include "code\datums\status_effects\stacking_effect.dm" #include "code\datums\status_effects\wound_effects.dm" +#include "code\datums\status_effects\buffs\stun_absorption.dm" #include "code\datums\status_effects\debuffs\blindness.dm" #include "code\datums\status_effects\debuffs\choke.dm" #include "code\datums\status_effects\debuffs\confusion.dm"