diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm index dce92503ba65..cc0d3d75d2f0 100644 --- a/code/__DEFINES/status_effects.dm +++ b/code/__DEFINES/status_effects.dm @@ -39,3 +39,67 @@ #define STASIS_ASCENSION_EFFECT "heretic_ascension" +// Status effect application helpers. +// These are macros for easier use of adjust_timed_status_effect and set_timed_status_effect. +// +// adjust_x: +// - Adds duration to a status effect +// - Removes duration if a negative duration is passed. +// - Ex: adjust_stutter(10 SECONDS) adds ten seconds of stuttering. +// - Ex: adjust_jitter(-5 SECONDS) removes five seconds of jittering, or just removes jittering if less than five seconds exist. +// +// adjust_x_up_to: +// - Will only add (or remove) duration of a status effect up to the second parameter +// - If the duration will result in going beyond the second parameter, it will stop exactly at that parameter +// - The second parameter cannot be negative. +// - Ex: adjust_stutter_up_to(20 SECONDS, 10 SECONDS) adds ten seconds of stuttering. +// +// set_x: +// - Set the duration of a status effect to the exact number. +// - Setting duration to zero seconds is effectively the same as just using remove_status_effect, or qdelling the effect. +// - Ex: set_stutter(10 SECONDS) sets the stuttering to ten seconds, regardless of whether they had more or less existing stutter. +// +// set_x_if_lower: +// - Will only set the duration of that effect IF any existing duration is lower than what was passed. +// - Ex: set_stutter_if_lower(10 SECONDS) will set stuttering to ten seconds if no stuttering or less than ten seconds of stuttering exists +// - Ex: set_jitter_if_lower(20 SECONDS) will do nothing if more than twenty seconds of jittering already exists + +#define adjust_stutter(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter) +#define adjust_stutter_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter, up_to) +#define set_stutter(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter) +#define set_stutter_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter, TRUE) + +#define adjust_derpspeech(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech) +#define adjust_derpspeech_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech, up_to) +#define set_derpspeech(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech) +#define set_derpspeech_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech, TRUE) + +#define adjust_slurring(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic) +#define adjust_slurring_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic, up_to) +#define set_slurring(duration) set_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic) +#define set_slurring_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic, TRUE) + +#define adjust_dizzy(duration) adjust_timed_status_effect(duration, /datum/status_effect/dizziness) +#define adjust_dizzy_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/dizziness, up_to) +#define set_dizzy(duration) set_timed_status_effect(duration, /datum/status_effect/dizziness) +#define set_dizzy_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/dizziness, TRUE) + +#define adjust_jitter(duration) adjust_timed_status_effect(duration, /datum/status_effect/jitter) +#define adjust_jitter_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/jitter, up_to) +#define set_jitter(duration) set_timed_status_effect(duration, /datum/status_effect/jitter) +#define set_jitter_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/jitter, TRUE) + +#define adjust_confusion(duration) adjust_timed_status_effect(duration, /datum/status_effect/confusion) +#define adjust_confusion_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/confusion, up_to) +#define set_confusion(duration) set_timed_status_effect(duration, /datum/status_effect/confusion) +#define set_confusion_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/confusion, TRUE) + +#define adjust_drugginess(duration) adjust_timed_status_effect(duration, /datum/status_effect/drugginess) +#define adjust_drugginess_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/drugginess, up_to) +#define set_drugginess(duration) set_timed_status_effect(duration, /datum/status_effect/drugginess) +#define set_drugginess_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/drugginess, TRUE) + +#define adjust_pacifism(duration) adjust_timed_status_effect(/datum/status_effect/pacify, duration) +#define set_pacifism(duration) set_timed_status_effect(/datum/status_effect/pacify, duration) + + diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index 0c396622d8a1..da5daae4df42 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -303,6 +303,11 @@ or shoot a gun to move around via Newton's 3rd Law of Motion." var/mob/living/carbon/offerer var/obj/item/receiving +/atom/movable/screen/alert/give/Destroy() + offerer = null + receiving = null + return ..() + /** * Handles assigning most of the variables for the alert that pops up when an item is offered * @@ -876,8 +881,8 @@ or shoot a gun to move around via Newton's 3rd Law of Motion." return TRUE /atom/movable/screen/alert/Destroy() - . = ..() severity = 0 master = null owner = null screen_loc = "" + return ..() diff --git a/code/datums/components/knockoff.dm b/code/datums/components/knockoff.dm index beb5db66dab8..b5563433c722 100644 --- a/code/datums/components/knockoff.dm +++ b/code/datums/components/knockoff.dm @@ -1,71 +1,99 @@ -///Items with these will have a chance to get knocked off when disarming or being knocked down +/// Items with this component will have a chance to get knocked off +/// (unequipped and sent to the ground) when the wearer is disarmed or knocked down. /datum/component/knockoff - ///Chance to knockoff + /// Chance to knockoff when a knockoff action occurs. var/knockoff_chance = 100 - ///Aiming for these zones will cause the knockoff, null means all zones allowed + /// Used in being disarmed. + /// If set, we will only roll the knockoff chance if the disarmer is targeting one of these zones. + /// If unset, any disarm act will cause the knock-off chance to be rolled, no matter the zone targeted. var/list/target_zones - ///Can be only knocked off from these slots, null means all slots allowed - var/list/slots_knockoffable + /// Bitflag used in equip to determine what slots we need to be in to be knocked off. + /// If set, we must be equipped in one of the slots to have a chance of our item being knocked off. + /// If unset / NONE, a disarm or knockdown will have a chance of our item being knocked off regardless of slot, INCLUDING hand slots. + var/slots_knockoffable = NONE -/datum/component/knockoff/Initialize(knockoff_chance,zone_override,slots_knockoffable) +/datum/component/knockoff/Initialize(knockoff_chance = 100, target_zones, slots_knockoffable = NONE) if(!isitem(parent)) return COMPONENT_INCOMPATIBLE - RegisterSignal(parent, COMSIG_ITEM_EQUIPPED,PROC_REF(OnEquipped)) - RegisterSignal(parent, COMSIG_ITEM_DROPPED,PROC_REF(OnDropped)) src.knockoff_chance = knockoff_chance + src.target_zones = target_zones + src.slots_knockoffable = slots_knockoffable - if(zone_override) - target_zones = zone_override +/datum/component/knockoff/RegisterWithParent() + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_dropped)) - if(slots_knockoffable) - src.slots_knockoffable = slots_knockoffable +/datum/component/knockoff/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED)) -///Tries to knockoff the item when disarmed -/datum/component/knockoff/proc/Knockoff(mob/living/carbon/human/wearer,mob/living/attacker,zone) + var/obj/item/item_parent = parent + if(ismob(item_parent.loc)) + UnregisterSignal(item_parent.loc, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN)) + +/// Signal proc for [COMSIG_HUMAN_DISARM_HIT] on the mob who's equipped our parent +/// Rolls a chance for knockoff whenever we're disarmed +/datum/component/knockoff/proc/on_equipped_mob_disarm(mob/living/carbon/human/source, mob/living/attacker, zone) SIGNAL_HANDLER - var/obj/item/item = parent - if(!istype(wearer)) + if(!istype(source)) return + if(target_zones && !(zone in target_zones)) return if(!prob(knockoff_chance)) return - if(!wearer.dropItemToGround(item)) + + var/obj/item/item_parent = parent + if(!source.dropItemToGround(item_parent)) return - wearer.visible_message(span_warning("[attacker] knocks off [wearer]'s [item.name]!"),span_userdanger("[attacker] knocks off your [item.name]!")) -///Tries to knockoff the item when user is knocked down -/datum/component/knockoff/proc/Knockoff_knockdown(mob/living/carbon/human/wearer,amount) + source.visible_message( + span_warning("[attacker] knocks off [source]'s [item_parent.name]!"), + span_userdanger("[attacker] knocks off your [item_parent.name]!"), + ) + +/// Signal proc for [COMSIG_LIVING_STATUS_KNOCKDOWN] on the mob who's equipped our parent +/// Rolls a chance for knockoff whenever we're knocked down +/datum/component/knockoff/proc/on_equipped_mob_knockdown(mob/living/carbon/human/source, amount) SIGNAL_HANDLER - if(amount <= 0) + if(!istype(source)) return - var/obj/item/item = parent - if(!istype(wearer)) + // Healing knockdown or setting knockdown to zero or something? Don't knock off. + if(amount <= 0) return if(!prob(knockoff_chance)) return - if(!wearer.dropItemToGround(item)) + + var/obj/item/item_parent = parent + if(!source.dropItemToGround(item_parent)) return - wearer.visible_message(span_warning("[wearer]'s [item.name] get[item.p_s()] knocked off!"),span_userdanger("Your [item.name] [item.p_were()] knocked off!")) + source.visible_message( + span_warning("[source]'s [item_parent.name] get[item_parent.p_s()] knocked off!"), + span_userdanger("Your [item_parent.name] [item_parent.p_were()] knocked off!"), + ) -/datum/component/knockoff/proc/OnEquipped(datum/source, mob/living/carbon/human/H,slot) +/// Signal proc for [COMSIG_ITEM_EQUIPPED] +/// Registers our signals which can cause a knockdown whenever we're equipped correctly +/datum/component/knockoff/proc/on_equipped(datum/source, mob/living/carbon/human/equipper, slot) SIGNAL_HANDLER - if(!istype(H)) + + if(!istype(equipper)) return - if(slots_knockoffable && !(slot in slots_knockoffable)) - UnregisterSignal(H, COMSIG_HUMAN_DISARM_HIT) - UnregisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN) + + if(slots_knockoffable && !(slot & slots_knockoffable)) + UnregisterSignal(equipper, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN)) return - RegisterSignal(H, COMSIG_HUMAN_DISARM_HIT, PROC_REF(Knockoff), TRUE) - RegisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN, PROC_REF(Knockoff_knockdown), TRUE) -/datum/component/knockoff/proc/OnDropped(datum/source, mob/living/M) + RegisterSignal(equipper, COMSIG_HUMAN_DISARM_HIT, PROC_REF(on_equipped_mob_disarm), TRUE) + RegisterSignal(equipper, COMSIG_LIVING_STATUS_KNOCKDOWN, PROC_REF(on_equipped_mob_knockdown), TRUE) + +/// Signal proc for [COMSIG_ITEM_DROPPED] +/// Unregisters our signals which can cause a knockdown when we're unequipped (dropped) +/datum/component/knockoff/proc/on_dropped(datum/source, mob/living/dropper) SIGNAL_HANDLER - UnregisterSignal(M, COMSIG_HUMAN_DISARM_HIT) - UnregisterSignal(M, COMSIG_LIVING_STATUS_KNOCKDOWN) + UnregisterSignal(dropper, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN)) diff --git a/code/datums/elements/frozen.dm b/code/datums/elements/frozen.dm index 51f879839c47..1504175208ec 100644 --- a/code/datums/elements/frozen.dm +++ b/code/datums/elements/frozen.dm @@ -13,6 +13,10 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0 if(target_obj.obj_flags & FREEZE_PROOF) return ELEMENT_INCOMPATIBLE + if(HAS_TRAIT(target_obj, TRAIT_FROZEN)) + return ELEMENT_INCOMPATIBLE + + ADD_TRAIT(target_obj, TRAIT_FROZEN, ELEMENT_TRAIT(type)) target_obj.name = "frozen [target_obj.name]" target_obj.add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY) target_obj.alpha -= 25 @@ -23,6 +27,7 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0 /datum/element/frozen/Detach(datum/source, ...) var/obj/obj_source = source + REMOVE_TRAIT(obj_source, TRAIT_FROZEN, ELEMENT_TRAIT(type)) obj_source.name = replacetext(obj_source.name, "frozen ", "") obj_source.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix) obj_source.alpha += 25 diff --git a/code/datums/quirks/negative.dm b/code/datums/quirks/negative.dm index f9f5582487ab..2a781bf00a76 100644 --- a/code/datums/quirks/negative.dm +++ b/code/datums/quirks/negative.dm @@ -511,7 +511,7 @@ if(prob(max(5,(nearby_people*12.5*moodmod)))) //Minimum 1/20 chance of stutter // Add a short stutter, THEN treat our word quirker.adjust_timed_status_effect(0.5 SECONDS, /datum/status_effect/speech/stutter) - new_message += quirker.treat_message(word) + new_message += quirker.treat_message(word, capitalize_message = FALSE) else new_message += word diff --git a/code/datums/status_effects/_status_effect.dm b/code/datums/status_effects/_status_effect.dm index 2cbae717e512..829e85eb4bf8 100644 --- a/code/datums/status_effects/_status_effect.dm +++ b/code/datums/status_effects/_status_effect.dm @@ -116,6 +116,7 @@ /// or when a status effect with on_remove_on_mob_delete /// set to FALSE has its mob deleted /datum/status_effect/proc/be_replaced() + linked_alert = null owner.clear_alert(id) LAZYREMOVE(owner.status_effects, src) owner = null diff --git a/code/modules/clothing/glasses/_glasses.dm b/code/modules/clothing/glasses/_glasses.dm index d57169f963c3..a0e9927b197f 100644 --- a/code/modules/clothing/glasses/_glasses.dm +++ b/code/modules/clothing/glasses/_glasses.dm @@ -241,7 +241,7 @@ /obj/item/clothing/glasses/regular/Initialize(mapload) . = ..() - AddComponent(/datum/component/knockoff,25,list(BODY_ZONE_PRECISE_EYES),list(ITEM_SLOT_EYES)) + AddComponent(/datum/component/knockoff, 25, list(BODY_ZONE_PRECISE_EYES), slot_flags) var/static/list/loc_connections = list( COMSIG_ATOM_ENTERED = PROC_REF(on_entered), ) diff --git a/code/modules/mob/living/brain/brain_say.dm b/code/modules/mob/living/brain/brain_say.dm index 71b73b940d09..ed3df59660c3 100644 --- a/code/modules/mob/living/brain/brain_say.dm +++ b/code/modules/mob/living/brain/brain_say.dm @@ -19,6 +19,7 @@ else return ..() -/mob/living/brain/treat_message(message) - message = capitalize(message) +/mob/living/brain/treat_message(message, capitalize_message = TRUE) + if(capitalize_message) + message = capitalize(message) return message diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index f1c6ebdd9039..2700bb0fa193 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -462,14 +462,20 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( return TRUE - -/mob/living/proc/treat_message(message) +/** + * Treats the passed message with things that may modify speech (stuttering, slurring etc). + * + * message - The message to treat. + * capitalize_message - Whether we run capitalize() on the message after we're done. + */ +/mob/living/proc/treat_message(message, capitalize_message = TRUE) if(HAS_TRAIT(src, TRAIT_UNINTELLIGIBLE_SPEECH)) message = unintelligize(message) SEND_SIGNAL(src, COMSIG_LIVING_TREAT_MESSAGE, args) - message = capitalize(message) + if(capitalize_message) + message = capitalize(message) return message diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm index fee2876ce676..2b47ff10cc17 100644 --- a/code/modules/mob/living/status_procs.dm +++ b/code/modules/mob/living/status_procs.dm @@ -80,7 +80,7 @@ return 0 /mob/living/proc/Knockdown(amount, ignore_canstun = FALSE) //Can't go below remaining duration - if(SEND_SIGNAL(src, /datum/status_effect/incapacitating/knockdown, amount, ignore_canstun) & COMPONENT_NO_STUN) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return if(IS_STUN_IMMUNE(src, ignore_canstun)) return diff --git a/code/modules/projectiles/aiming.dm b/code/modules/projectiles/aiming.dm index 88e21c7c6337..8b40dc161b4c 100644 --- a/code/modules/projectiles/aiming.dm +++ b/code/modules/projectiles/aiming.dm @@ -30,6 +30,7 @@ register_to_target(target) + RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(target_del)) RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(target_del)) RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(check_sight)) RegisterSignal(user, COMSIG_MOB_FIRED_GUN, PROC_REF(user_shot)) diff --git a/code/modules/reagents/chemistry/reagents/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drink_reagents.dm index a7bf91ce6650..82a292c603e2 100644 --- a/code/modules/reagents/chemistry/reagents/drink_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drink_reagents.dm @@ -677,7 +677,7 @@ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/consumable/tonic/on_mob_life(mob/living/carbon/M, delta_time, times_fired) - M.adjust_timed_status_effect(10 SECONDS * REM * delta_time, /datum/status_effect/dizziness) + M.adjust_timed_status_effect(-10 SECONDS * REM * delta_time, /datum/status_effect/dizziness) M.adjust_drowsyness(-3 * REM * delta_time) M.AdjustSleeping(-40 * REM * delta_time) M.adjust_bodytemperature(-5 * REM * TEMPERATURE_DAMAGE_COEFFICIENT * delta_time, M.get_body_temp_normal()) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index b18c5d5787b6..7a47b6873f0d 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -104,6 +104,7 @@ #include "hydroponics_harvest.dm" #include "hydroponics_self_mutations.dm" #include "keybinding_init.dm" +#include "knockoff_component.dm" #include "load_map_security.dm" #include "machine_disassembly.dm" #include "mapping.dm" diff --git a/code/modules/unit_tests/knockoff_component.dm b/code/modules/unit_tests/knockoff_component.dm new file mode 100644 index 000000000000..eda04576118a --- /dev/null +++ b/code/modules/unit_tests/knockoff_component.dm @@ -0,0 +1,79 @@ +/// Test that the knockoff component will properly cause something +/// with it applied to be knocked off when it should be. +/datum/unit_test/knockoff_component + +/datum/unit_test/knockoff_component/Run() + var/mob/living/carbon/human/wears_the_glasses = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/shoves_the_guy = allocate(/mob/living/carbon/human) + + // No pre-existing items have a 100% chance of being knocked off, + // so we'll just apply it to a relatively generic item (glasses) + var/obj/item/clothing/glasses/sunglasses/glasses = allocate(/obj/item/clothing/glasses/sunglasses) + glasses.AddComponent(/datum/component/knockoff, \ + knockoff_chance = 100, \ + target_zones = list(BODY_ZONE_PRECISE_EYES), \ + slots_knockoffable = glasses.slot_flags) + + // Save this for later, since we wanna reset our dummy positions even after they're shoved about. + var/turf/right_of_shover = locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z) + + // Position shover (bottom left) and the shovee (1 tile right of bottom left, no wall behind them) + shoves_the_guy.forceMove(run_loc_floor_bottom_left) + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy failed to equip the glasses.") + + // Test disarm, targeting chest + // A disarm targeting chest should not knockdown or lose glasses + shoves_the_guy.zone_selected = BODY_ZONE_CHEST + shoves_the_guy.disarm(wears_the_glasses) + TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down when being disarmed shouldn't have been.") + TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy lost their glasses even thought they were disarmed targeting the wrong slot.") + + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + // Test disarm, targeting eyes + // A disarm targeting eyes should not knockdown but should lose glasses + shoves_the_guy.zone_selected = BODY_ZONE_PRECISE_EYES + shoves_the_guy.disarm(wears_the_glasses) + TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down when being disarmed shouldn't have been.") + TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they were shoved targeting the correct zone.") + + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + // Test Knockdown() + // Any amount of positive Kockdown should lose glasses + wears_the_glasses.Knockdown(1 SECONDS) + TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after Knockdown() was called.") + TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by Knockdown().") + + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + // Test AdjustKnockdown() + // Any amount of positive Kockdown should lose glasses + wears_the_glasses.AdjustKnockdown(1 SECONDS) + TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after AdjustKnockdown() was called.") + TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by AdjustKnockdown().") + + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + // Test SetKnockdown() + // Any amount of positive Kockdown should lose glasses + wears_the_glasses.SetKnockdown(1 SECONDS) + TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after SetKnockdown() was called.") + TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by SetKnockdown().") + + set_glasses_wearer(wears_the_glasses, right_of_shover, glasses) + + // Test a negative value applied of Knockdown (AdjustKnockdown, SetKnockdown, and Knockdown should all act the same here) + // Any amount of negative Kockdown should not cause the glasses to be lost + wears_the_glasses.AdjustKnockdown(-1 SECONDS) + TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down after AdjustKnockdown() was called with a negative value.") + TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy lost their glasses, even though AdjustKnockdown() was called with a negative value.") + +/// Helper to reset the glasses dummy back to it's original position, clear knockdown, and return glasses (if gone) +/datum/unit_test/knockoff_component/proc/set_glasses_wearer(mob/living/carbon/human/wearer, turf/reset_to, obj/item/clothing/glasses/reset_worn) + wearer.forceMove(reset_to) + wearer.SetKnockdown(0 SECONDS) + if(!wearer.glasses) + wearer.equip_to_slot_if_possible(reset_worn, ITEM_SLOT_EYES)