From 22799fcb892f8ba8f0f4f9a26bd8b792e30cc8b2 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Fri, 16 Jun 2023 15:44:25 -0500 Subject: [PATCH] Refactors the worst list ever, Stun Absorptions, into status effects + makes status flags more accurate (making certain mobs more vulnerable to incapacitations?) (#76000) ## About The Pull Request - Refactors the stun absorption list into a status effect - Does a fair bit of cleanup around stun code Weird thing involved in this. Check out this define. `IS_STUN_IMMUNE(source, ignore_canstun) ((source.status_flags & GODMODE) || (!ignore_canstun && (!(source.status_flags & CANKNOCKDOWN) || HAS_TRAIT(source, TRAIT_STUNIMMUNE))))` Notice anything odd about it? It only checks for `CANKNOCKDOWN`. What does this mean? Well, *every single* one of the stun procs used this macro for checking stun immunity. Which means every method of stun checked the `CANKNOCKDOWN`. This means that, say you have a mob which has `CANSTUN` but not `CANKNOCKDOWN`. Intuitively this means that the mob cannot be knocked down, but can be stunned. But instead, this means the mob can't be stunned either. This doesn't affect humans, they have all the status flags, but it does affect some other mobs. Alien adults (not queens) have `CANUNCONSCIOUS|CANPUSH`. Before, they didn't have `CANKNOCKDOWN`, so they were fully immune to stuns and sleeps. But now, they can be knocked unconscious. However, overall it doesn't change much, as most mobs that flipped off `CANKNOCKDOWN` flipped off the others too. For consistency though it makes sense for these flags to work as they imply. - `incapacitate` didn't have a signal, now it does ## Why It's Good For The Game More consistent, better code? I may use this in the future. ## Changelog :cl: Melbert refactor: Refactored Stun Absorptions (Bastard Sword, His Grace) refactor: Refactored Stun Immunity. Note this means that some mobs which, prior, were immune to all forms of incapacitation are now vulnerable to some. Notably, adult non-queen xenomorphs are now vulnerable to falling unconscious. /:cl: --- code/__DEFINES/combat.dm | 15 ++ .../signals/signals_mob/signals_mob_living.dm | 4 + code/__DEFINES/traits.dm | 3 +- code/datums/status_effects/buffs.dm | 61 +++-- .../status_effects/buffs/stun_absorption.dm | 235 ++++++++++++++++++ code/game/objects/items/weaponry.dm | 12 +- .../antagonists/cult/cult_bastard_sword.dm | 9 +- .../antagonists/highlander/highlander.dm | 2 +- code/modules/clothing/head/jobs.dm | 2 +- code/modules/clothing/under/costume.dm | 2 +- code/modules/hallucination/stray_bullet.dm | 2 +- .../mob/living/carbon/human/examine.dm | 5 - .../modules/mob/living/carbon/status_procs.dm | 5 +- code/modules/mob/living/damage_procs.dm | 2 +- .../mob/living/silicon/robot/robot_model.dm | 2 +- .../hostile/megafauna/blood_drunk_miner.dm | 5 +- code/modules/mob/living/status_procs.dm | 140 ++++------- .../projectiles/projectile/energy/stun.dm | 2 +- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/stuns.dm | 72 ++++++ tgstation.dme | 1 + 21 files changed, 438 insertions(+), 144 deletions(-) create mode 100644 code/datums/status_effects/buffs/stun_absorption.dm create mode 100644 code/modules/unit_tests/stuns.dm 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"