mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-30 16:47:13 +01:00
Handcuffs can now be used to bind certain items (briefcases, toolboxes, etc.) to your hand. (#93305)
## About The Pull Request Technically, this PR introduces the cuffable_item element and the cuffed_item status effect and their relative code. In more player-friendly terms, this allows the ability to use handcuffs to bind certain items to your hands by right-clicking it with a pair of handcuffs in your active hand. This makes the item unable to be dropped, for better or worse, until you or someone else remove said cuffs. And no, this doesn't conflict with the ability to be handcuffed if you're silly enough to think that. There are more than one way to remove the cuffs. For the player with the item cuffed to their hand, to remove the cuffs they can either click the status alert, or examine the item and click the relative hyperlink. The second option is good to have if for some reason the status alert doesn't show up (too many alerts etc.). For other people, they can remove the cuffs by opening the strip inventory menu (the one you open by click-dragging the sprite of person with the item onto yours). It's an alternative action specific to this status effect (therefore only held items). Until the cuffs are removed, trying to remove the item **directly** will bring you nowhere **because the item is stuck to their hands**, duh. Alternatively you can just chop their arm off. You do what you do. For a list of items that can be bound with cuffs (suggestions welcome): - briefcases - toolboxes - lockboxes - first aid kits - shields (they generally have handles and all. gameplay-wise they already take away one hand slot to use. Using cuffs seals the deal: no swapping items on the go, so no two-handed weapons, but you won't drop the shield until it's broken) - jerrycans (Kryson's suggestion) - soup pots (ditto, kinda weird) - coffee mugs, and the mauna mug (ditto) - buckets - plushes (silly stuff, if you ever want to arrest a plush or test the feature) - pet carriers - mining drills - swords with closed guards (ERT chainsaw-sword, cap's sabre, parsnip sabre, cutlass, e-cutlass...) - crutches and the white cane - baskets - flashlights and lamps (not subtypes like flares, glowsticks and torches) - TTVs - chairs ## Why It's Good For The Game This opens up for some emergent use for handcuffs beside people (or prisoner shoes). Inspired by a scene of some 1998 action movie, where one of the bad guys had the mc guffin briefcase latched to his wrist with a pair of handcuffs. Codewise, it was also a reason to refactor bits of code like handcuffs and screen alerts slightly. On a sidenote, actual sprites for cult/heretic shackles. ## Changelog 🆑 add: You can now bind certain items like briefcases, toolboxes, medkits, shields, jerrycans etc. to your hand with a pair of handcuffs, preventing them from being dropped. You can remove said binds at any time unless incapacitated, and so can others through the strip inventory menu. qol: The appearance of a screen alert now updates if the object it represents (like, an item offered by another player) changes appearance. imageadd: The shadow shackles item (from cult magic and heretic sacrifices) now has its own icon. /🆑
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
///This element allows the item it's attached to be bound to oneself's arm with a pair of handcuffs (sold separately). Borgs need not to apply
|
||||
/datum/element/cuffable_item
|
||||
|
||||
/datum/element/cuffable_item/Attach(datum/target)
|
||||
. = ..()
|
||||
|
||||
if(!isitem(target))
|
||||
return ELEMENT_INCOMPATIBLE
|
||||
|
||||
RegisterSignal(target, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examine_more))
|
||||
RegisterSignal(target, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(item_interaction))
|
||||
|
||||
var/atom/atom_target = target
|
||||
atom_target.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
|
||||
RegisterSignal(atom_target, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
|
||||
|
||||
///Tell the player about the interaction if they examine the item twice.
|
||||
/datum/element/cuffable_item/proc/on_examine_more(obj/item/source, mob/user, list/examine_list)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
if(length(user.held_items) < 0 || iscyborg(user) || source.anchored)
|
||||
return
|
||||
examine_list += span_smallnotice("You could bind [source.p_they()] to your wrist with a pair of handcuffs...")
|
||||
|
||||
///Give context to players holding a pair of handcuffs when hovering the item
|
||||
/datum/element/cuffable_item/proc/on_requesting_context_from_item(datum/source, list/context, obj/item/held_item, mob/user)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
if (!istype(held_item, /obj/item/restraints/handcuffs))
|
||||
return NONE
|
||||
var/obj/item/restraints/handcuffs/cuffs = held_item
|
||||
if(!cuffs.used)
|
||||
context[SCREENTIP_CONTEXT_RMB] = "Cuff to your wrist"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
/datum/element/cuffable_item/proc/item_interaction(obj/item/source, mob/living/user, obj/item/tool, modifiers)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
if(!istype(tool, /obj/item/restraints/handcuffs) || iscyborg(user) || source.anchored || !user.CanReach(source))
|
||||
return NONE
|
||||
|
||||
INVOKE_ASYNC(src, PROC_REF(apply_cuffs), source, user, tool)
|
||||
return ITEM_INTERACT_SUCCESS
|
||||
|
||||
///The proc responsible for adding the status effect to the player and all...
|
||||
/datum/element/cuffable_item/proc/apply_cuffs(obj/item/source, mob/living/user, obj/item/restraints/handcuffs/cuffs)
|
||||
if(cuffs.used || DOING_INTERACTION_WITH_TARGET(user, source))
|
||||
return
|
||||
|
||||
if(HAS_TRAIT_FROM(source, TRAIT_NODROP, CUFFED_ITEM_TRAIT))
|
||||
to_chat(user, span_warning("[source] is already cuffed to your wrist!"))
|
||||
return
|
||||
|
||||
if(cuffs.handcuffs_clumsiness_check(user))
|
||||
return
|
||||
|
||||
source.balloon_alert(user, "cuffing item...")
|
||||
playsound(source, cuffs.cuffsound, 30, TRUE, -2)
|
||||
if(!do_after(user, cuffs.get_handcuff_time(user), source))
|
||||
return
|
||||
|
||||
playsound(source, cuffs.cuffsuccesssound, 30, TRUE, -2)
|
||||
|
||||
if(user.apply_status_effect(/datum/status_effect/cuffed_item, source, cuffs))
|
||||
source.balloon_alert(user, "item cuffed to wrist")
|
||||
return
|
||||
|
||||
source.balloon_alert(user, "couldn't cuff to wrist!")
|
||||
return
|
||||
@@ -49,12 +49,16 @@
|
||||
UnregisterSignal(target, list(COMSIG_ITEM_ATTACK_SECONDARY, COMSIG_ATOM_EXAMINE, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET))
|
||||
return ..()
|
||||
|
||||
/datum/element/cuffsnapping/proc/add_item_context(obj/item/source, list/context, mob/living/carbon/target, mob/living/user)
|
||||
/datum/element/cuffsnapping/proc/add_item_context(obj/item/source, list/context, mob/living/target, mob/living/user)
|
||||
SIGNAL_HANDLER
|
||||
if(!iscarbon(target) || !target.handcuffed)
|
||||
return NONE
|
||||
context[SCREENTIP_CONTEXT_RMB] = "Cut Restraints"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
if(iscarbon(target)) //Removing restraints takes precedence
|
||||
var/mob/living/carbon/carbon_target = target
|
||||
if(carbon_target.handcuffed)
|
||||
context[SCREENTIP_CONTEXT_RMB] = "Cut Restraints"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
if(target.has_status_effect(/datum/status_effect/cuffed_item))
|
||||
context[SCREENTIP_CONTEXT_RMB] = "Remove Binds From Item"
|
||||
return CONTEXTUAL_SCREENTIP_SET
|
||||
|
||||
///signal called on parent being examined
|
||||
/datum/element/cuffsnapping/proc/on_examine(datum/target, mob/user, list/examine_list)
|
||||
@@ -72,48 +76,74 @@
|
||||
|
||||
examine_list += span_notice(examine_string)
|
||||
|
||||
/datum/element/cuffsnapping/proc/try_cuffsnap_target(obj/item/cutter, mob/living/carbon/target, mob/living/cutter_user, list/modifiers)
|
||||
///Signal called on parent when it right-clicks another mob.
|
||||
/datum/element/cuffsnapping/proc/try_cuffsnap_target(obj/item/cutter, mob/living/target, mob/living/cutter_user, list/modifiers)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
if(!istype(target)) //we aren't the kind of mob that can even have cuffs, so we skip.
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
var/mob/living/carbon/carbon_target = target
|
||||
if(!istype(carbon_target) || !carbon_target.handcuffed)
|
||||
var/datum/status_effect/cuffed_item/cuffed_status = target.has_status_effect(/datum/status_effect/cuffed_item)
|
||||
if(!cuffed_status)
|
||||
return NONE
|
||||
INVOKE_ASYNC(src, PROC_REF(try_cuffsnap_item), cutter, target, cutter_user, cuffed_status.cuffed, cuffed_status.cuffs)
|
||||
return COMPONENT_SKIP_ATTACK
|
||||
|
||||
var/obj/item/restraints/handcuffs/cuffs = carbon_target.handcuffed
|
||||
|
||||
if(!istype(cuffs))
|
||||
return NONE
|
||||
|
||||
if(check_cuffs_strength(carbon_target, target, cutter_user, cuffs, span_notice("[cutter_user] tries to cut through [target]'s restraints with [cutter], but fails!")))
|
||||
INVOKE_ASYNC(src, PROC_REF(do_cuffsnap_target), cutter, target, cutter_user, cuffs)
|
||||
|
||||
return COMPONENT_SKIP_ATTACK
|
||||
|
||||
///Check that the type of restraints can be cut by this element.
|
||||
/datum/element/cuffsnapping/proc/check_cuffs_strength(obj/item/cutter, mob/living/target, mob/living/cutter_user, obj/item/restraints/handcuffs/cuffs, message)
|
||||
if(cuffs.restraint_strength ? snap_time_strong : snap_time_weak)
|
||||
return TRUE
|
||||
cutter_user.visible_message(message)
|
||||
playsound(source = get_turf(cutter), soundin = cutter.usesound || cutter.hitsound, vol = cutter.get_clamped_volume(), vary = TRUE)
|
||||
return FALSE
|
||||
|
||||
///Called when a player tries to remove the cuffs restraining another mob.
|
||||
/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
|
||||
do_snip_snap(cutter, target, cutter_user, cuffs, span_notice("[cutter_user] cuts [target]'s restraints with [cutter]!"))
|
||||
|
||||
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)
|
||||
///Called when a player tries to remove the cuffs binding an item to their owner
|
||||
/datum/element/cuffsnapping/proc/try_cuffsnap_item(obj/item/cutter, mob/living/target, mob/living/cutter_user, obj/item/cuffed, obj/item/restraints/handcuffs/cuffs)
|
||||
if(check_cuffs_strength(cutter, target, cutter_user, cuffs, span_notice("[cutter_user] tries to cut through the restraints binding [cuffed] to [target], but fails!")))
|
||||
return
|
||||
|
||||
return
|
||||
log_combat(cutter_user, target, "cut or tried to cut restraints binding [cuffed] to")
|
||||
|
||||
do_snip_snap(cutter, target, cutter_user, cuffs, span_notice("[cutter_user] cuts the restraints binding [src] to [target] with [cutter]!"))
|
||||
|
||||
///The proc responsible for the very timed action that deletes the cuffs
|
||||
/datum/element/cuffsnapping/proc/do_snip_snap(obj/item/cutter, mob/living/target, mob/cutter_user, obj/item/restraints/handcuffs/cuffs, message)
|
||||
var/snap_time = cuffs.restraint_strength ? snap_time_strong : snap_time_weak
|
||||
|
||||
var/target_was_restrained = FALSE
|
||||
if(iscarbon(target))
|
||||
var/mob/living/carbon/carbon_target = target
|
||||
target_was_restrained = carbon_target.handcuffed
|
||||
|
||||
if(snap_time)
|
||||
if(!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.
|
||||
return
|
||||
if(target_was_restrained) //Removing restraints takes priority over cuffed items. This only applies for carbon mobs, but we need to make sure the restraints are still the same.
|
||||
var/mob/living/carbon/carbon_target = target
|
||||
if(carbon_target.handcuffed != cuffs)
|
||||
return
|
||||
|
||||
cutter_user.do_attack_animation(target, used_item = cutter)
|
||||
cutter_user.visible_message(message)
|
||||
qdel(cuffs)
|
||||
playsound(source = get_turf(cutter), soundin = cutter.usesound || cutter.hitsound, vol = cutter.get_clamped_volume(), vary = TRUE)
|
||||
|
||||
@@ -182,9 +182,14 @@
|
||||
* All string keys in the list must be inside tgui\packages\tgui\interfaces\StripMenu.tsx
|
||||
* You can also return null if there are no alternate actions.
|
||||
*/
|
||||
/datum/strippable_item/proc/get_alternate_actions(atom/source, mob/user)
|
||||
/datum/strippable_item/proc/get_alternate_actions(atom/source, mob/user, obj/item/item)
|
||||
RETURN_TYPE(/list)
|
||||
return null
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
|
||||
var/list/alt_actions = list()
|
||||
if(item)
|
||||
SEND_SIGNAL(item, COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS, source, user, alt_actions)
|
||||
return alt_actions
|
||||
|
||||
/**
|
||||
* Performs an alternate action on this strippable_item.
|
||||
@@ -193,9 +198,9 @@
|
||||
* - action_key: The key of the alternate action to perform.
|
||||
* Returns FALSE if unable to perform the action; whether it be due to the signal or some other factor.
|
||||
*/
|
||||
/datum/strippable_item/proc/perform_alternate_action(atom/source, mob/user, action_key)
|
||||
/datum/strippable_item/proc/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
if(SEND_SIGNAL(user, COMSIG_TRY_ALT_ACTION, source, action_key) & COMPONENT_CANT_ALT_ACTION)
|
||||
if(item && SEND_SIGNAL(item, COMSIG_ITEM_STRIPPABLE_ALT_ACTION, source, user, action_key) & COMPONENT_ALT_ACTION_DONE)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
@@ -375,7 +380,8 @@
|
||||
|
||||
result["icon"] = icon2base64(icon(item.icon, item.icon_state))
|
||||
result["name"] = item.name
|
||||
result["alternate"] = item_data.get_alternate_actions(owner, user)
|
||||
var/list/alt_actions = item_data.get_alternate_actions(owner, user, item)
|
||||
result["alternate"] = length(alt_actions) ? alt_actions : null
|
||||
var/static/list/already_cried = list()
|
||||
if(length(result["alternate"]) > 3 && !(type in already_cried))
|
||||
stack_trace("Too many alternate actions for [type]! Only three are supported at the moment! This will look bad!")
|
||||
@@ -486,16 +492,14 @@
|
||||
return
|
||||
|
||||
var/item = strippable_item.get_item(owner)
|
||||
if (isnull(item))
|
||||
return
|
||||
|
||||
if (!(alt_action in strippable_item.get_alternate_actions(owner, user)))
|
||||
if (!(alt_action in strippable_item.get_alternate_actions(owner, user, item)))
|
||||
return
|
||||
|
||||
LAZYORASSOCLIST(interactions, user, key)
|
||||
|
||||
// Potentially yielding
|
||||
strippable_item.perform_alternate_action(owner, user, alt_action)
|
||||
strippable_item.perform_alternate_action(owner, user, alt_action, item)
|
||||
|
||||
LAZYREMOVEASSOC(interactions, user, key)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user