diff --git a/code/datums/elements/cuffsnapping.dm b/code/datums/elements/cuffsnapping.dm new file mode 100644 index 00000000000..004b0410573 --- /dev/null +++ b/code/datums/elements/cuffsnapping.dm @@ -0,0 +1,109 @@ +/** + * cuffsnapping element replaces the item's secondary attack with an aimed attack at the kneecaps under certain circumstances. + * + * Element is incompatible with non-items. Requires the parent item to have a force equal to or greater than WOUND_MINIMUM_DAMAGE. + * Also requires that the parent can actually get past pre_secondary_attack without the attack chain cancelling. + * + * cuffsnapping attacks have a wounding bonus between severe and critical+10 wound thresholds. Without some serious wound protecting + * armour this all but guarantees a wound of some sort. The attack is directed specifically at a limb and the limb takes the damage. + * + * Requires the cutter_user to be aiming for either leg zone, which will be targetted specifically. They will than have a 3-second long + * do_after before executing the attack. + * + * cuffsnapping requires the target to either be on the floor, immobilised or buckled to something. And also to have an appropriate leg. + * + * Passing all the checks will cancel the entire attack chain. + */ + +/** + * Cuffsnapping element! When added to an item allows it to attempt to break cuffs. + * Depending on certain parameters and variables it might only be able to cut through cable, or take time, etc. + * + * Element is only compatible with items. + */ + +/datum/element/cuffsnapping + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 // let bos cutters paeper cutters and etc do it too + /// If not null, can snap cable restraints and similar. + var/snap_time_weak = 0 SECONDS + /// If not null, can snap handcuffs. + var/snap_time_strong = null + /// Note: As of time of writing (5/9/23) it takes 4 seconds to manually remove handcuffs. Anything above that value is a waste of time. + +/datum/element/cuffsnapping/Attach(datum/target, snap_time_weak = 0 SECONDS, snap_time_strong = null) + . = ..() + + if(!isitem(target)) + stack_trace("cuffsnapping element added to non-item object: \[[target]\]") + return ELEMENT_INCOMPATIBLE + + src.snap_time_weak = snap_time_weak + src.snap_time_strong = snap_time_strong + + RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(target, COMSIG_ITEM_ATTACK , PROC_REF(try_cuffsnap_target)) + +/datum/element/cuffsnapping/Detach(datum/target) + UnregisterSignal(target, list(COMSIG_ITEM_ATTACK, COMSIG_PARENT_EXAMINE)) + + return ..() + +///signal called on parent being examined +/datum/element/cuffsnapping/proc/on_examine(datum/target, mob/user, list/examine_list) + SIGNAL_HANDLER + + var/examine_string + if(isnull(snap_time_weak)) + return + examine_string = "It looks like it could cut zipties or cable restraints off someone in [snap_time_weak] seconds" + + if(!isnull(snap_time_strong)) + examine_string += ", and handcuffs in [snap_time_strong] seconds." + else + examine_string += "." + + examine_list += span_notice(examine_string) + +/datum/element/cuffsnapping/proc/try_cuffsnap_target(obj/item/cutter, mob/living/carbon/target, mob/cutter_user, params) + SIGNAL_HANDLER + + if(!target.handcuffed) + return + + var/obj/item/restraints/handcuffs/cuffs = target.handcuffed + + if(!istype(cuffs)) + return + + if(cuffs.restraint_strength && isnull(src.snap_time_strong)) + cutter_user.visible_message(span_notice("[cutter_user] tries to cut through [target]'s restraints with [cutter], but fails!")) + playsound(source = get_turf(cutter), soundin = cutter.usesound ? cutter.usesound : cutter.hitsound, vol = cutter.get_clamped_volume(), vary = TRUE) + return COMPONENT_SKIP_ATTACK + + else if(isnull(src.snap_time_weak)) + cutter_user.visible_message(span_notice("[cutter_user] tries to cut through [target]'s restraints with [cutter], but fails!")) + playsound(source = get_turf(cutter), soundin = cutter.usesound ? cutter.usesound : cutter.hitsound, vol = cutter.get_clamped_volume(), vary = TRUE) + return COMPONENT_SKIP_ATTACK + + . = COMPONENT_SKIP_ATTACK + + INVOKE_ASYNC(src, PROC_REF(do_cuffsnap_target), cutter, target, cutter_user, cuffs) + +/datum/element/cuffsnapping/proc/do_cuffsnap_target(obj/item/cutter, mob/living/carbon/target, mob/cutter_user, obj/item/restraints/handcuffs/cuffs) + if(LAZYACCESS(cutter_user.do_afters, cutter)) + return + + log_combat(cutter_user, target, "cut or tried to cut [target]'s cuffs", cutter) + + var/snap_time = src.snap_time_weak + if(cuffs.restraint_strength) + snap_time = src.snap_time_strong + + if(snap_time == 0 || do_after(cutter_user, snap_time, target, interaction_key = cutter)) // If 0 just do it. This to bypass the do_after() creating a needless progress bar. + cutter_user.do_attack_animation(target, used_item = cutter) + cutter_user.visible_message(span_notice("[cutter_user] cuts [target]'s restraints with [cutter]!")) + qdel(target.handcuffed) + playsound(source = get_turf(cutter), soundin = cutter.usesound ? cutter.usesound : cutter.hitsound, vol = cutter.get_clamped_volume(), vary = TRUE) + + return diff --git a/code/game/objects/items/boxcutter.dm b/code/game/objects/items/boxcutter.dm index 7b7749a63b3..ebf5eac5bbc 100644 --- a/code/game/objects/items/boxcutter.dm +++ b/code/game/objects/items/boxcutter.dm @@ -17,6 +17,10 @@ /// Whether or not the boxcutter has been readied var/on = FALSE var/on_sound = 'sound/items/boxcutter_activate.ogg' + /// Used on Initialize, how much time to cut cable restraints and zipties. + var/snap_time_weak_handcuffs = 0 SECONDS + /// Used on Initialize, how much time to cut real handcuffs. Null means it can't. + var/snap_time_strong_handcuffs = null /obj/item/boxcutter/Initialize(mapload) . = ..() @@ -46,5 +50,8 @@ on = active playsound(src, on_sound, 50) tool_behaviour = (active ? TOOL_KNIFE : NONE) + if(active) + AddElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs) + else + RemoveElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs) return COMPONENT_NO_DEFAULT_MESSAGE - diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm index d482639d2a3..87e8d06db41 100644 --- a/code/game/objects/items/handcuffs.dm +++ b/code/game/objects/items/handcuffs.dm @@ -7,6 +7,7 @@ * 1. A special suicide * 2. If a restraint is handcuffing/legcuffing a carbon while being deleted, it will remove the handcuff/legcuff status. */ + /obj/item/restraints breakouttime = 1 MINUTES dye_color = DYE_PRISONER @@ -16,6 +17,11 @@ user.visible_message(span_suicide("[user] is strangling [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!")) return OXYLOSS +// Zipties, cable cuffs, etc. Can be cut with wirecutters instantly. +#define HANDCUFFS_TYPE_WEAK 0 +// Handcuffs... alien handcuffs. Can be cut through only by jaws of life. +#define HANDCUFFS_TYPE_STRONG 1 + /** * # Handcuffs * @@ -46,6 +52,8 @@ var/cuffsound = 'sound/weapons/handcuffs.ogg' ///If set, handcuffs will be destroyed on application and leave behind whatever this is set to. var/trashtype = null + /// How strong the cuffs are. Weak cuffs can be broken with wirecutters or boxcutters. + var/restraint_strength = HANDCUFFS_TYPE_STRONG /datum/armor/restraints_handcuffs fire = 50 @@ -132,6 +140,7 @@ name = "fake handcuffs" desc = "Fake handcuffs meant for gag purposes." breakouttime = 1 SECONDS + restraint_strength = HANDCUFFS_TYPE_WEAK /** * # Cable restraints @@ -151,6 +160,7 @@ custom_materials = list(/datum/material/iron= SMALL_MATERIAL_AMOUNT * 1.5, /datum/material/glass= SMALL_MATERIAL_AMOUNT * 0.75) breakouttime = 30 SECONDS cuffsound = 'sound/weapons/cablecuff.ogg' + restraint_strength = HANDCUFFS_TYPE_WEAK /obj/item/restraints/handcuffs/cable/Initialize(mapload, new_color) . = ..() diff --git a/code/game/objects/items/tools/crowbar.dm b/code/game/objects/items/tools/crowbar.dm index a705fa3500b..9203c8a123e 100644 --- a/code/game/objects/items/tools/crowbar.dm +++ b/code/game/objects/items/tools/crowbar.dm @@ -102,6 +102,10 @@ w_class = WEIGHT_CLASS_NORMAL toolspeed = 0.7 force_opens = TRUE + /// Used on Initialize, how much time to cut cable restraints and zipties. + var/snap_time_weak_handcuffs = 0 SECONDS + /// Used on Initialize, how much time to cut real handcuffs. Null means it can't. + var/snap_time_strong_handcuffs = 0 SECONDS /obj/item/crowbar/power/Initialize(mapload) . = ..() @@ -126,6 +130,10 @@ tool_behaviour = (active ? TOOL_WIRECUTTER : TOOL_CROWBAR) balloon_alert(user, "attached [active ? "cutting" : "prying"]") playsound(user ? user : src, 'sound/items/change_jaws.ogg', 50, TRUE) + if(tool_behaviour == TOOL_CROWBAR) + RemoveElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs) + else + AddElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs) return COMPONENT_NO_DEFAULT_MESSAGE /obj/item/crowbar/power/syndicate @@ -155,14 +163,6 @@ playsound(loc, SFX_DESECRATION, 50, TRUE, -1) return BRUTELOSS -/obj/item/crowbar/power/attack(mob/living/carbon/attacked_carbon, mob/user) - if(istype(attacked_carbon) && attacked_carbon.handcuffed && tool_behaviour == TOOL_WIRECUTTER) - user.visible_message(span_notice("[user] cuts [attacked_carbon]'s restraints with [src]!")) - qdel(attacked_carbon.handcuffed) - return - - return ..() - /obj/item/crowbar/cyborg name = "hydraulic crowbar" desc = "A hydraulic prying tool, simple but powerful." diff --git a/code/game/objects/items/tools/wirecutters.dm b/code/game/objects/items/tools/wirecutters.dm index 5f3dc8c9a8a..df597bbcfc4 100644 --- a/code/game/objects/items/tools/wirecutters.dm +++ b/code/game/objects/items/tools/wirecutters.dm @@ -41,6 +41,10 @@ COLOR_TOOL_CYAN, COLOR_TOOL_YELLOW, ) + /// Used on Initialize, how much time to cut cable restraints and zipties. + var/snap_time_weak_handcuffs = 0 SECONDS + /// Used on Initialize, how much time to cut real handcuffs. Null means it can't. + var/snap_time_strong_handcuffs = null /datum/armor/item_wirecutters fire = 50 @@ -51,15 +55,7 @@ set_greyscale(colors = list(pick(wirecutter_colors))) AddElement(/datum/element/falling_hazard, damage = force, wound_bonus = wound_bonus, hardhat_safety = TRUE, crushes = FALSE, impact_sound = hitsound) - - return ..() - -/obj/item/wirecutters/attack(mob/living/carbon/attacked_carbon, mob/user) - if(istype(attacked_carbon) && attacked_carbon.handcuffed && istype(attacked_carbon.handcuffed, /obj/item/restraints/handcuffs/cable)) - user.visible_message(span_notice("[user] cuts [attacked_carbon]'s restraints with [src]!")) - qdel(attacked_carbon.handcuffed) - return - + AddElement(/datum/element/cuffsnapping, snap_time_weak_handcuffs, snap_time_strong_handcuffs) return ..() /obj/item/wirecutters/suicide_act(mob/living/user) @@ -75,6 +71,7 @@ icon_state = "cutters" toolspeed = 0.1 random_color = FALSE + snap_time_strong_handcuffs = 1 SECONDS /obj/item/wirecutters/cyborg name = "powered wirecutters" diff --git a/tgstation.dme b/tgstation.dme index e718badc41d..617cc44155a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1169,6 +1169,7 @@ #include "code\datums\elements\content_barfer.dm" #include "code\datums\elements\crackable.dm" #include "code\datums\elements\crusher_loot.dm" +#include "code\datums\elements\cuffsnapping.dm" #include "code\datums\elements\cult_eyes.dm" #include "code\datums\elements\cult_halo.dm" #include "code\datums\elements\curse_announcement.dm"