diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index 0b75f01a07d..6117c1c55d0 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -1,6 +1,3 @@
-///Called on user, from base of /datum/strippable_item/perform_alternate_action() (atom/target, action_key)
-#define COMSIG_TRY_ALT_ACTION "try_alt_action"
- #define COMPONENT_CANT_ALT_ACTION (1<<0)
///Called on /basic when updating its speed, from base of /mob/living/basic/update_basic_mob_varspeed(): ()
#define POST_BASIC_MOB_UPDATE_VARSPEED "post_basic_mob_update_varspeed"
///from base of /mob/Login(): ()
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 726e30989d3..f9d8dcbfa9e 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -154,6 +154,19 @@
///from base of datum/storage/handle_exit(): (datum/storage/storage)
#define COMSIG_ITEM_UNSTORED "item_unstored"
+/**
+ * From base of datum/strippable_item/get_alternate_actions(): (atom/owner, mob/user, list/alt_actions)
+ * As a side note, make sure the strippable item datum (the slot) in question doesn't have too many alternate actions already,
+ * as only up to three are supported at a time (as of september 2025), though, so far only the jumpsuit slot uses all three slots.
+ *
+ * Also make sure to code the alt action and add it to the StripMenu.tsx interface
+ */
+#define COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS "item_get_strippable_alt_actions"
+
+/// From base of datum/strippable_item/perform_alternate_action(): (atom/owner, mob/user, action_key)
+#define COMSIG_ITEM_STRIPPABLE_ALT_ACTION "item_strippable_alt_action"
+ #define COMPONENT_ALT_ACTION_DONE (1<<0)
+
///from base of obj/item/apply_fantasy_bonuses(): (bonus)
#define COMSIG_ITEM_APPLY_FANTASY_BONUSES "item_apply_fantasy_bonuses"
///from base of obj/item/remove_fantasy_bonuses(): (bonus)
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index ae0a1e24145..86d1c82a7f7 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -107,6 +107,9 @@
/// Trait given to you by shapeshifting
#define SHAPESHIFT_TRAIT "shapeshift_trait"
+///From the cuffed_item status effect
+#define CUFFED_ITEM_TRAIT "cuffed_item_trait"
+
// unique trait sources, still defines
#define EMP_TRAIT "emp_trait"
#define STATUE_MUTE "statue"
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index c10bc5922e2..cb4cd48b730 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -12,13 +12,13 @@
*flicks are forwarded to master
*override makes it so the alert is not replaced until cleared by a clear_alert with clear_override, and it's used for hallucinations.
*/
-/mob/proc/throw_alert(category, type, severity, obj/new_master, override = FALSE, timeout_override, no_anim = FALSE)
+/mob/proc/throw_alert(category, type, severity, atom/new_master, override = FALSE, timeout_override, no_anim = FALSE)
if(!category || QDELETED(src))
return
var/datum/weakref/master_ref
- if(isdatum(new_master))
+ if(isatom(new_master))
master_ref = WEAKREF(new_master)
var/atom/movable/screen/alert/thealert
if(alerts[category])
@@ -51,17 +51,9 @@
thealert.owner = src
if(new_master)
- var/mutable_appearance/master_appearance = new(new_master)
- master_appearance.appearance_flags = KEEP_TOGETHER
- master_appearance.layer = FLOAT_LAYER
- master_appearance.plane = FLOAT_PLANE
- master_appearance.dir = SOUTH
- master_appearance.pixel_x = new_master.base_pixel_x
- master_appearance.pixel_y = new_master.base_pixel_y
- master_appearance.pixel_z = new_master.base_pixel_z
- thealert.add_overlay(strip_appearance_underlays(master_appearance))
- thealert.icon_state = "template" // We'll set the icon to the client's ui pref in reorganize_alerts()
thealert.master_ref = master_ref
+ thealert.RegisterSignal(new_master, COMSIG_ATOM_UPDATE_APPEARANCE, TYPE_PROC_REF(/atom/movable/screen/alert, on_master_update_appearance))
+ thealert.update_appearance(UPDATE_OVERLAYS)
else
thealert.icon_state = "[initial(thealert.icon_state)][severity]"
thealert.severity = severity
@@ -118,6 +110,9 @@
/// Boolean. If TRUE, the Click() proc will attempt to Click() on the master first if there is a master.
var/click_master = TRUE
+ ///If set true, instead of using the default icon file for screen alerts, it will use the hud's ui style
+ var/use_user_hud_icon = FALSE
+
/atom/movable/screen/alert/Initialize(mapload, datum/hud/hud_owner)
. = ..()
if(clickable_glow)
@@ -129,10 +124,63 @@
if(!QDELETED(src))
openToolTip(usr,src,params,title = name,content = desc,theme = alerttooltipstyle)
-
/atom/movable/screen/alert/MouseExited()
closeToolTip(usr)
+/atom/movable/screen/alert/proc/on_master_update_appearance(datum/source)
+ SIGNAL_HANDLER
+ update_appearance(UPDATE_OVERLAYS)
+
+/atom/movable/screen/alert/update_overlays()
+ . = ..()
+ var/atom/our_master = master_ref?.resolve()
+ if(!istype(our_master) || QDELETED(our_master))
+ return
+ . += add_atom_icon(our_master)
+
+///Returns a copy of the appearance of the atom, with its base pixel coordinates. Useful for overlays
+/atom/movable/screen/alert/proc/add_atom_icon(atom/atom)
+ var/mutable_appearance/atom_appearance = new(atom)
+ atom_appearance.appearance_flags = KEEP_TOGETHER
+ atom_appearance.layer = FLOAT_LAYER
+ atom_appearance.plane = FLOAT_PLANE
+ atom_appearance.dir = SOUTH
+ atom_appearance.pixel_x = atom.base_pixel_x
+ atom_appearance.pixel_y = atom.base_pixel_y
+ atom_appearance.pixel_w = atom.base_pixel_w
+ atom_appearance.pixel_z = atom.base_pixel_z
+ return atom_appearance
+
+/atom/movable/screen/alert/Click(location, control, params)
+ SHOULD_CALL_PARENT(TRUE)
+
+ ..()
+ if(!usr || !usr.client)
+ return FALSE
+ if(usr != owner)
+ return FALSE
+ var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, SHIFT_CLICK)) // screen objects don't do the normal Click() stuff so we'll cheat
+ to_chat(usr, boxed_message(jointext(examine(usr), "\n")))
+ return FALSE
+ if(!click_master)
+ return TRUE
+ var/datum/our_master = master_ref?.resolve()
+ if(our_master)
+ return usr.client.Click(our_master, location, control, params)
+
+/atom/movable/screen/alert/Destroy()
+ . = ..()
+ severity = 0
+ master_ref = null
+ owner = null
+ screen_loc = ""
+
+/atom/movable/screen/alert/examine(mob/user)
+ return list(
+ span_boldnotice(name),
+ span_info(desc),
+ )
//Gas alerts
// Gas alerts are continuously thrown/cleared by:
@@ -326,7 +374,8 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
return roller.resist_fire()
/atom/movable/screen/alert/give // information set when the give alert is made
- icon_state = "default"
+ icon_state = "template"
+ use_user_hud_icon = TRUE
clickable_glow = TRUE
/// The offer we're linked to, yes this is suspiciously like a status effect alert
var/datum/status_effect/offering/offer
@@ -848,6 +897,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
name = "Something interesting is happening!"
desc = "This can be clicked on to perform an action."
icon_state = "template"
+ use_user_hud_icon = TRUE
timeout = 30 SECONDS
clickable_glow = TRUE
/// Weakref to the target atom to use the action on
@@ -875,6 +925,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
/atom/movable/screen/alert/poll_alert
name = "Looking for candidates"
icon_state = "template"
+ use_user_hud_icon = TRUE
timeout = 30 SECONDS
ghost_screentips = TRUE
/// If true you need to call START_PROCESSING manually
@@ -1030,6 +1081,8 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
clickable_glow = TRUE
/atom/movable/screen/alert/restrained
+ icon_state = "template"
+ use_user_hud_icon = TRUE
clickable_glow = TRUE
/atom/movable/screen/alert/restrained/handcuffed
@@ -1128,7 +1181,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
return TRUE
for(var/i in 1 to length(alerts))
var/atom/movable/screen/alert/alert = alerts[alerts[i]]
- if(alert.icon_state == "template")
+ if(alert.use_user_hud_icon)
alert.icon = ui_style
alert.screen_loc = get_ui_alert_placement(i)
screenmob.client.screen |= alert
@@ -1136,34 +1189,3 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
for(var/viewer in mymob.observers)
reorganize_alerts(viewer)
return TRUE
-
-/atom/movable/screen/alert/Click(location, control, params)
- SHOULD_CALL_PARENT(TRUE)
-
- ..()
- if(!usr || !usr.client)
- return FALSE
- if(usr != owner)
- return FALSE
- var/list/modifiers = params2list(params)
- if(LAZYACCESS(modifiers, SHIFT_CLICK)) // screen objects don't do the normal Click() stuff so we'll cheat
- to_chat(usr, boxed_message(jointext(examine(usr), "\n")))
- return FALSE
- var/datum/our_master = master_ref?.resolve()
- if(our_master && click_master)
- return usr.client.Click(our_master, location, control, params)
-
- return TRUE
-
-/atom/movable/screen/alert/Destroy()
- . = ..()
- severity = 0
- master_ref = null
- owner = null
- screen_loc = ""
-
-/atom/movable/screen/alert/examine(mob/user)
- return list(
- span_boldnotice(name),
- span_info(desc),
- )
diff --git a/code/datums/components/aura_healing.dm b/code/datums/components/aura_healing.dm
index e71a06de2fb..3f23b161adb 100644
--- a/code/datums/components/aura_healing.dm
+++ b/code/datums/components/aura_healing.dm
@@ -158,5 +158,7 @@
/atom/movable/screen/alert/aura_healing
name = "Aura Healing"
icon_state = "template"
+ use_user_hud_icon = TRUE
+ clickable_glow = TRUE
#undef HEAL_EFFECT_COOLDOWN
diff --git a/code/datums/elements/cuffable_item.dm b/code/datums/elements/cuffable_item.dm
new file mode 100644
index 00000000000..d52d2a1057f
--- /dev/null
+++ b/code/datums/elements/cuffable_item.dm
@@ -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
diff --git a/code/datums/elements/cuffsnapping.dm b/code/datums/elements/cuffsnapping.dm
index 3640b0a5bd8..9c0b3d73935 100644
--- a/code/datums/elements/cuffsnapping.dm
+++ b/code/datums/elements/cuffsnapping.dm
@@ -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)
diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm
index 85f2fe6ca60..55c0fa2d7c7 100644
--- a/code/datums/elements/strippable.dm
+++ b/code/datums/elements/strippable.dm
@@ -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)
diff --git a/code/datums/status_effects/cuffed_item.dm b/code/datums/status_effects/cuffed_item.dm
new file mode 100644
index 00000000000..c3843b1a4cf
--- /dev/null
+++ b/code/datums/status_effects/cuffed_item.dm
@@ -0,0 +1,165 @@
+/**
+ * The status effect given by the cuffable_item.
+ * It basically binds an item to your arm, basically making it undroppable until the cuffs or item are removed, usually done by one of:
+ * - clicking the status alert
+ * - using the topic hyperlink
+ * - strip menu for others
+ * - alternatively, dismemberment or destroying the item
+ */
+/datum/status_effect/cuffed_item
+ id = "cuffed_item"
+ status_type = STATUS_EFFECT_MULTIPLE
+ alert_type = /atom/movable/screen/alert/status_effect/cuffed_item
+ ///Reference to the item stuck into the player's hand
+ var/obj/item/cuffed
+ ///Reference to the pair of handcuffs used to bind the item
+ var/obj/item/restraints/handcuffs/cuffs
+
+/datum/status_effect/cuffed_item/on_creation(mob/living/new_owner, obj/item/cuffed, obj/item/restraints/handcuffs/cuffs)
+ src.cuffed = cuffed
+ src.cuffs = cuffs
+ . = ..() //throws the alert and all
+ linked_alert.update_appearance(UPDATE_OVERLAYS)
+
+/datum/status_effect/cuffed_item/on_apply()
+ if(HAS_TRAIT_FROM(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT))
+ qdel(src)
+ return FALSE
+ owner.temporarilyRemoveItemFromInventory(cuffs, force = TRUE)
+ if(!owner.is_holding(cuffed) && !owner.put_in_hands(cuffed))
+ owner.put_in_hands(cuffs)
+ qdel(src)
+ return FALSE
+
+ ADD_TRAIT(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT)
+
+ RegisterSignals(cuffed, list(COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_displaced))
+ RegisterSignal(cuffed, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
+ RegisterSignal(cuffed, COMSIG_ATOM_EXAMINE, PROC_REF(cuffed_reminder))
+ RegisterSignal(cuffed, COMSIG_TOPIC, PROC_REF(topic_handler))
+ RegisterSignal(cuffed, COMSIG_ITEM_GET_STRIPPABLE_ALT_ACTIONS, PROC_REF(get_strippable_action))
+ RegisterSignal(cuffed, COMSIG_ITEM_STRIPPABLE_ALT_ACTION, PROC_REF(do_strippable_action))
+
+ RegisterSignals(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_displaced))
+ RegisterSignal(cuffs, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(on_item_update_appearance))
+
+ RegisterSignal(owner, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examine_more))
+
+ owner.log_message("bound [src] to themselves with restraints", LOG_GAME)
+
+ return TRUE
+
+/datum/status_effect/cuffed_item/on_remove()
+ //Prevent possible recursions from these signals
+ UnregisterSignal(cuffed, list(COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
+ UnregisterSignal(cuffs, list(COMSIG_ITEM_EQUIPPED, COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
+
+ REMOVE_TRAIT(cuffed, TRAIT_NODROP, CUFFED_ITEM_TRAIT)
+ cuffed = null
+
+ if(!QDELETED(cuffs))
+ cuffs.on_uncuffed(wearer = owner)
+ if(!QDELETED(owner) && cuffs.loc == owner && !(cuffs in owner.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD)))
+ cuffs.forceMove(owner.drop_location())
+ cuffs = null
+
+///Called when someone examines the owner twice, so they can know if someone has a cuffed item
+/datum/status_effect/cuffed_item/proc/on_examine_more(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ examine_list += span_warning("[cuffed.examine_title(user)] is bound to [owner.p_their()] [owner.get_held_index_name(owner.get_held_index_of_item(cuffed))] by [cuffs.examine_title(user)]")
+
+///What happens if one of the items is moved away from the mob
+/datum/status_effect/cuffed_item/proc/on_displaced(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+///Tell the player that the item is stuck to their hands someway. Also another way to trigger the try_remove_cuffs proc.
+/datum/status_effect/cuffed_item/proc/cuffed_reminder(obj/item/item, mob/user, list/examine_texts)
+ SIGNAL_HANDLER
+
+ if(user == owner)
+ examine_texts += span_notice("[item.p_Theyre()] cuffed to you by \a [cuffs]. You can remove them.")
+
+/// This mainly exists as a fallback in the rare case the alert icon is not reachable (too many alerts?). You should be somewhat able to examine items while blind so all good.
+/datum/status_effect/cuffed_item/proc/topic_handler(atom/source, user, href_list)
+ SIGNAL_HANDLER
+
+ if(user == owner && href_list["remove_cuffs_item"])
+ INVOKE_ASYNC(src, PROC_REF(try_remove_cuffs), user)
+
+/datum/status_effect/cuffed_item/proc/get_strippable_action(obj/item/source, atom/owner, mob/user, list/alt_actions)
+ SIGNAL_HANDLER
+ alt_actions += "remove_item_cuffs"
+
+/datum/status_effect/cuffed_item/proc/do_strippable_action(obj/item/source, atom/owner, mob/user, action_key)
+ SIGNAL_HANDLER
+ if(action_key != "remove_item_cuffs")
+ return NONE
+ if(source != cuffed || !isliving(user))
+ return NONE
+
+ INVOKE_ASYNC(src, PROC_REF(try_remove_cuffs), user)
+ return COMPONENT_ALT_ACTION_DONE
+
+///The main proc responsible for attempting to remove the hancfuss.
+/datum/status_effect/cuffed_item/proc/try_remove_cuffs(mob/living/user)
+
+ var/interaction_key = REF(src)
+ if(LAZYACCESS(user.do_afters, interaction_key))
+ return FALSE
+
+ if(!(user.mobility_flags & MOBILITY_USE) || (user != owner && !user.CanReach(owner)))
+ owner.balloon_alert(user, "can't do it right now!")
+ return FALSE
+
+ if(user != owner)
+ owner.visible_message(span_notice("[user] tries to remove [cuffs] binding [cuffed] to [owner]"), span_warning("[user] is trying to remove [cuffs] binding [cuffed] to you."))
+
+ owner.balloon_alert(user, "removing cuffs...")
+ playsound(owner, cuffs.cuffsound, 30, TRUE, -2)
+ if(!do_after(user, cuffs.get_handcuff_time(user) * 1.5, owner, interaction_key = interaction_key) || QDELETED(src))
+ owner.balloon_alert(user, "interrupted!")
+ return FALSE
+
+ if(user != owner)
+ owner.visible_message(span_notice("[user] removes [cuffs] binding [cuffed] to [owner]"), span_warning("[user] removes [cuffs] binding [cuffed] to you."))
+
+ log_combat(user, owner, "removed restraints binding [cuffed] to")
+
+ var/obj/item/restraints/handcuffs/ref_cuffs = cuffs
+ ref_cuffs.forceMove(owner.drop_location()) //This will cause the status effect to delete itself, which unsets the 'cuffs' var
+ user.put_in_hands(ref_cuffs)
+ owner.balloon_alert(user, "cuffs removed from item")
+
+ return TRUE
+
+///Whenever the appearance of one of either cuffed or cuffs is updated, update the alert appearance
+/datum/status_effect/cuffed_item/proc/on_item_update_appearance(datum/source)
+ SIGNAL_HANDLER
+ linked_alert.update_appearance(UPDATE_OVERLAYS)
+
+///The status alert linked to the cuffed_item status effect
+/atom/movable/screen/alert/status_effect/cuffed_item
+ name = "Cuffed Item"
+ desc = "You've an item firmly cuffed to your arm. You probably won't be accidentally dropping it somewhere anytime soon."
+ icon_state = "template"
+ use_user_hud_icon = TRUE
+ clickable_glow = TRUE
+ click_master = FALSE
+
+/atom/movable/screen/alert/status_effect/cuffed_item/update_overlays()
+ . = ..()
+ if(!attached_effect)
+ return
+ var/datum/status_effect/cuffed_item/effect = attached_effect
+ . += add_atom_icon(effect.cuffed)
+ var/mutable_appearance/cuffs_appearance = add_atom_icon(effect.cuffs)
+ cuffs_appearance.transform *= 0.8
+ . += cuffs_appearance
+
+/atom/movable/screen/alert/status_effect/cuffed_item/Click(location, control, params)
+ . = ..()
+ if(.)
+ var/datum/status_effect/cuffed_item/effect = attached_effect
+ effect?.try_remove_cuffs(owner)
diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm
index f395b4acbd6..4ee61917f16 100644
--- a/code/game/objects/items/devices/flashlight.dm
+++ b/code/game/objects/items/devices/flashlight.dm
@@ -37,9 +37,13 @@
var/start_on = FALSE
/// When true, painting the flashlight won't change its light color
var/ignore_base_color = FALSE
+ /// This simply means if the flashlight can be cuffed to your hand (why?)
+ var/has_closed_handle = TRUE
/obj/item/flashlight/Initialize(mapload)
. = ..()
+ if(has_closed_handle)
+ AddElement(/datum/element/cuffable_item)
if(start_on)
set_light_on(TRUE)
update_brightness()
@@ -328,6 +332,7 @@
light_range = 2
light_power = 0.8
light_color = "#CCFFFF"
+ has_closed_handle = FALSE
COOLDOWN_DECLARE(holosign_cooldown)
/obj/item/flashlight/pen/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
@@ -385,6 +390,7 @@
light_power = 0.8
light_color = "#99ccff"
hitsound = 'sound/items/weapons/genhit1.ogg'
+ has_closed_handle = FALSE
// the desk lamps are a bit special
/obj/item/flashlight/lamp
@@ -402,6 +408,7 @@
obj_flags = CONDUCTS_ELECTRICITY
custom_materials = null
start_on = TRUE
+ has_closed_handle = FALSE
// green-shaded desk lamp
/obj/item/flashlight/lamp/green
@@ -434,6 +441,7 @@
grind_results = list(/datum/reagent/sulfur = 15)
sound_on = 'sound/items/match_strike.ogg'
toggle_context = FALSE
+ has_closed_handle = FALSE
/// How many seconds of fuel we have left
var/fuel = 0
/// Do we randomize the fuel when initialized
@@ -770,6 +778,7 @@
light_range = 6 //luminosity when on
light_color = "#ffff66"
light_system = OVERLAY_LIGHT
+ has_closed_handle = FALSE
/obj/item/flashlight/emp
var/emp_max_charges = 4
@@ -844,6 +853,7 @@
sound_on = 'sound/effects/wounds/crack2.ogg' // the cracking sound isn't just for wounds silly
toggle_context = FALSE
ignore_base_color = TRUE
+ has_closed_handle = FALSE
/// How much max fuel we have
var/max_fuel = 0
/// How much oxygen gets added upon cracking the stick. Doesn't actually produce a reaction with the fluid but it does allow for bootleg chemical "grenades"
@@ -1023,6 +1033,7 @@
plane = FLOOR_PLANE
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ has_closed_handle = FALSE
///Boolean that switches when a full color flip ends, so the light can appear in all colors.
var/even_cycle = FALSE
///Base light_range that can be set on Initialize to use in smooth light range expansions and contractions.
diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm
index aee6e25b832..dc87b76644f 100644
--- a/code/game/objects/items/devices/transfer_valve.dm
+++ b/code/game/objects/items/devices/transfer_valve.dm
@@ -24,6 +24,7 @@
/obj/item/transfer_valve/Initialize(mapload)
. = ..()
+ AddElement(/datum/element/cuffable_item)
RegisterSignal(src, COMSIG_ITEM_FRIED, PROC_REF(on_fried))
register_context()
register_item_context()
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index 0210ef078a0..f4c36f02456 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -55,17 +55,16 @@
///How long it takes to handcuff someone
var/handcuff_time = 4 SECONDS
- ///Multiplier for handcuff time
- var/handcuff_time_mod = 1
///Sound that plays when starting to put handcuffs on someone
var/cuffsound = 'sound/items/weapons/handcuffs.ogg'
///Sound that plays when restrain is successful
var/cuffsuccesssound = 'sound/items/handcuff_finish.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
+ /// Is this pair of cuff being actually used?
+ var/used = FALSE
+
/obj/item/restraints/handcuffs/apply_fantasy_bonuses(bonus)
. = ..()
handcuff_time = modify_fantasy_variable("handcuff_time", handcuff_time, -bonus * 2, minimum = 0.3 SECONDS)
@@ -79,7 +78,7 @@
acid = 50
/obj/item/restraints/handcuffs/attack(mob/living/target_mob, mob/living/user)
- if(!iscarbon(target_mob))
+ if(!iscarbon(target_mob) || used)
return
attempt_to_cuff(target_mob, user)
@@ -90,9 +89,7 @@
victim.balloon_alert(user, "can't be handcuffed!")
return
- if(iscarbon(user) && (HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50))) //Clumsy people have a 50% chance to handcuff themselves instead of their target.
- to_chat(user, span_warning("Uh... how do those things work?!"))
- apply_cuffs(user, user)
+ if(handcuffs_clumsiness_check(user))
return
if(!isnull(victim.handcuffed))
@@ -114,12 +111,7 @@
playsound(loc, cuffsound, 30, TRUE, -2)
log_combat(user, victim, "attempted to handcuff")
- if(HAS_TRAIT(user, TRAIT_FAST_CUFFING))
- handcuff_time_mod = 0.75
- else
- handcuff_time_mod = 1
-
- if(!do_after(user, handcuff_time * handcuff_time_mod, victim, timed_action_flags = IGNORE_SLOWDOWNS) || !victim.canBeHandcuffed())
+ if(!do_after(user, get_handcuff_time(user), victim, timed_action_flags = IGNORE_SLOWDOWNS) || !victim.canBeHandcuffed())
victim.balloon_alert(user, "failed to handcuff!")
to_chat(user, span_warning("You fail to handcuff [victim]!"))
log_combat(user, victim, "failed to handcuff")
@@ -136,7 +128,16 @@
log_combat(user, victim, "successfully handcuffed")
SSblackbox.record_feedback("tally", "handcuffs", 1, type)
+///Return the amount of time the user would spend cuffing someone or something
+/obj/item/restraints/handcuffs/proc/get_handcuff_time(mob/user)
+ return handcuff_time * (HAS_TRAIT(user, TRAIT_FAST_CUFFING) ? 0.75 : 1)
+/obj/item/restraints/handcuffs/proc/handcuffs_clumsiness_check(mob/user)
+ if(!iscarbon(user) || !HAS_TRAIT(user, TRAIT_CLUMSY) || prob(50)) //Clumsy people have a 50% chance to handcuff themselves instead of their target.
+ return FALSE
+ to_chat(user, span_warning("Uh... how do those things work?!"))
+ apply_cuffs(user, user)
+ return TRUE
/**
* When called, this instantly puts handcuffs on someone (if actually possible)
*
@@ -153,16 +154,24 @@
return
var/obj/item/restraints/handcuffs/cuffs = src
- if(trashtype)
- cuffs = new trashtype()
- else if(dispense)
+ if(dispense)
cuffs = new type()
target.equip_to_slot(cuffs, ITEM_SLOT_HANDCUFFED)
- if(trashtype && !dispense)
+ if(dispense)
qdel(src)
+/obj/item/restraints/handcuffs/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot == ITEM_SLOT_HANDCUFFED)
+ RegisterSignal(src, COMSIG_ITEM_DROPPED, PROC_REF(on_uncuffed)) //Make sure zipties are no longer usable the next time someone removes them
+
+/obj/item/restraints/handcuffs/proc/on_uncuffed(datum/source, mob/living/wearer)
+ SIGNAL_HANDLER
+ SHOULD_CALL_PARENT(TRUE)
+ UnregisterSignal(src, COMSIG_ITEM_DROPPED)
+
/**
* # Alien handcuffs
*
@@ -342,10 +351,15 @@
righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
custom_materials = null
breakouttime = 45 SECONDS
- trashtype = /obj/item/restraints/handcuffs/cable/zipties/used
color = null
cable_color = null
+/obj/item/restraints/handcuffs/cable/zipties/on_uncuffed(datum/source, mob/living/wearer)
+ . = ..()
+ desc = "A pair of broken zipties."
+ icon_state = "cuff_used"
+ used = TRUE
+
/**
* # Used zipties
*
@@ -354,9 +368,7 @@
/obj/item/restraints/handcuffs/cable/zipties/used
desc = "A pair of broken zipties."
icon_state = "cuff_used"
-
-/obj/item/restraints/handcuffs/cable/zipties/used/attack()
- return
+ used = TRUE
/**
* # Fake Zipties
@@ -372,6 +384,21 @@
/obj/item/restraints/handcuffs/cable/zipties/fake/used
desc = "A pair of broken fake zipties."
icon_state = "cuff_used"
+ used = TRUE
+
+///handcuffs applied by cult magic and heretics sacrifice
+/obj/item/restraints/handcuffs/cult
+ name = "shadow shackles"
+ desc = "Shackles that bind the wrists with sinister magic."
+ breakouttime = 45 SECONDS
+ icon_state = "cult_shackles"
+ flags_1 = NONE
+
+/obj/item/restraints/handcuffs/cult/on_uncuffed(datum/source, mob/living/wearer)
+ . = ..()
+ wearer.visible_message(span_danger("[wearer]'s shackles shatter in a discharge of dark magic!"), span_userdanger("Your [src] shatters in a discharge of dark magic!"))
+ qdel(src)
+
/**
* # Generic leg cuffs
diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm
index c4ed1c0e344..0d3e5d11ea1 100644
--- a/code/game/objects/items/melee/energy.dm
+++ b/code/game/objects/items/melee/energy.dm
@@ -329,6 +329,10 @@
righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
light_color = COLOR_RED
+/obj/item/melee/energy/sword/pirate/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //closed sword guard
+
/// Energy blades, which are effectively perma-extended energy swords
/obj/item/melee/energy/blade
name = "energy blade"
diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm
index feac13198ce..f8ffa76b320 100644
--- a/code/game/objects/items/melee/misc.dm
+++ b/code/game/objects/items/melee/misc.dm
@@ -60,6 +60,7 @@
/obj/item/melee/sabre/Initialize(mapload)
. = ..()
+ AddElement(/datum/element/cuffable_item) //closed sword guard
AddComponent(/datum/component/jousting)
//fast and effective, but as a sword, it might damage the results.
AddComponent(/datum/component/butchering, \
@@ -173,6 +174,7 @@
/obj/item/melee/parsnip_sabre/Initialize(mapload)
. = ..()
+ AddElement(/datum/element/cuffable_item) //closed sword guard
AddComponent(/datum/component/jousting)
/obj/item/melee/parsnip_sabre/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
diff --git a/code/game/objects/items/pet_carrier.dm b/code/game/objects/items/pet_carrier.dm
index ee3b323eec0..dc93e936439 100644
--- a/code/game/objects/items/pet_carrier.dm
+++ b/code/game/objects/items/pet_carrier.dm
@@ -47,6 +47,7 @@
/obj/item/pet_carrier/Initialize(mapload)
. = ..()
register_context()
+ AddElement(/datum/element/cuffable_item)
/obj/item/pet_carrier/Destroy()
if(occupants.len)
diff --git a/code/game/objects/items/plushes.dm b/code/game/objects/items/plushes.dm
index f733481ae9b..1e928edf989 100644
--- a/code/game/objects/items/plushes.dm
+++ b/code/game/objects/items/plushes.dm
@@ -47,6 +47,7 @@
AddComponent(/datum/component/squeak, squeak_override)
AddElement(/datum/element/bed_tuckable, mapload, 6, -5, 90)
AddElement(/datum/element/toy_talk)
+ AddElement(/datum/element/cuffable_item)
//have we decided if Pinocchio goes in the blue or pink aisle yet?
if(gender == NEUTER)
diff --git a/code/game/objects/items/shields.dm b/code/game/objects/items/shields.dm
index 19727a38a23..a54c3bf46eb 100644
--- a/code/game/objects/items/shields.dm
+++ b/code/game/objects/items/shields.dm
@@ -43,6 +43,7 @@
/obj/item/shield/Initialize(mapload)
. = ..()
AddElement(/datum/element/disarm_attack)
+ AddElement(/datum/element/cuffable_item) //I mean, it has a closed handle, right?
/obj/item/shield/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
var/effective_block_chance = final_block_chance
diff --git a/code/game/objects/items/storage/basket.dm b/code/game/objects/items/storage/basket.dm
index ab33ea67d84..4859bd6b598 100644
--- a/code/game/objects/items/storage/basket.dm
+++ b/code/game/objects/items/storage/basket.dm
@@ -7,3 +7,6 @@
resistance_flags = FLAMMABLE
storage_type = /datum/storage/basket
+/obj/item/storage/basket/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
diff --git a/code/game/objects/items/storage/briefcase.dm b/code/game/objects/items/storage/briefcase.dm
index dc710870c56..16c6a02468b 100644
--- a/code/game/objects/items/storage/briefcase.dm
+++ b/code/game/objects/items/storage/briefcase.dm
@@ -21,6 +21,10 @@
/// The path of the folder that gets spawned in New()
var/folder_path = /obj/item/folder
+/obj/item/storage/briefcase/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/obj/item/storage/briefcase/PopulateContents()
new /obj/item/pen(src)
var/obj/item/folder/folder = new folder_path(src)
diff --git a/code/game/objects/items/storage/lockbox.dm b/code/game/objects/items/storage/lockbox.dm
index 02f89c0f351..791be925adf 100644
--- a/code/game/objects/items/storage/lockbox.dm
+++ b/code/game/objects/items/storage/lockbox.dm
@@ -22,6 +22,7 @@
register_context()
update_icon_state()
+ AddElement(/datum/element/cuffable_item)
///screentips for lockboxes
/obj/item/storage/lockbox/add_context(atom/source, list/context, obj/item/held_item, mob/user)
diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm
index a4ad08cbbf1..8efce4eefc7 100644
--- a/code/game/objects/items/storage/medkit.dm
+++ b/code/game/objects/items/storage/medkit.dm
@@ -27,6 +27,10 @@
/// Defines damage type of the medkit. General ones stay null. Used for medibot healing bonuses
var/damagetype_healed
+/obj/item/storage/medkit/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/obj/item/storage/medkit/regular
icon_state = "medkit"
desc = "A first aid kit with the ability to heal common types of injuries."
diff --git a/code/game/objects/items/storage/toolboxes/_toolbox.dm b/code/game/objects/items/storage/toolboxes/_toolbox.dm
index 1a078bebec8..e0986a941a6 100644
--- a/code/game/objects/items/storage/toolboxes/_toolbox.dm
+++ b/code/game/objects/items/storage/toolboxes/_toolbox.dm
@@ -46,6 +46,7 @@
latches = "quad_latch" // like winning the lottery, but worse
update_appearance()
AddElement(/datum/element/falling_hazard, damage = force, wound_bonus = wound_bonus, hardhat_safety = TRUE, crushes = FALSE, impact_sound = hitsound)
+ AddElement(/datum/element/cuffable_item)
/obj/item/storage/toolbox/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if (user.combat_mode || !user.has_hand_for_held_index(user.get_inactive_hand_index()))
diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm
index 3f48cad55fc..56137231941 100644
--- a/code/game/objects/items/weaponry.dm
+++ b/code/game/objects/items/weaponry.dm
@@ -173,6 +173,10 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
throw_range = 5
armour_penetration = 35
+/obj/item/claymore/cutlass/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //closed sword guard
+
/obj/item/claymore/cutlass/old
name = "old cutlass"
desc = parent_type::desc + " This one seems a tad old."
@@ -672,6 +676,10 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
attack_verb_continuous = list("bludgeons", "whacks", "thrashes")
attack_verb_simple = list("bludgeon", "whack", "thrash")
+/obj/item/cane/crutch/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/obj/item/cane/crutch/examine(mob/user, thats)
. = ..()
// tacked on after the cane string
@@ -724,6 +732,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
/obj/item/cane/white/Initialize(mapload)
. = ..()
+ AddElement(/datum/element/cuffable_item)
AddComponent( \
/datum/component/transforming, \
force_on = 7, \
diff --git a/code/game/objects/structures/beds_chairs/chair.dm b/code/game/objects/structures/beds_chairs/chair.dm
index f01698e6bb8..d5fc803ed99 100644
--- a/code/game/objects/structures/beds_chairs/chair.dm
+++ b/code/game/objects/structures/beds_chairs/chair.dm
@@ -428,6 +428,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/chair/stool/bar, 0)
// What structure type does this chair become when placed?
var/obj/structure/chair/origin_type = /obj/structure/chair
+/obj/item/chair/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/obj/item/chair/suicide_act(mob/living/carbon/user)
user.visible_message(span_suicide("[user] begins hitting [user.p_them()]self with \the [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
playsound(src,hitsound,50,TRUE)
@@ -442,25 +446,28 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/chair/stool/bar, 0)
plant(user)
/obj/item/chair/proc/plant(mob/user)
- var/turf/T = get_turf(loc)
- if(isgroundlessturf(T))
+ var/turf/turf = user.loc
+ if(!istype(turf) || isgroundlessturf(turf))
to_chat(user, span_warning("You need ground to plant this on!"))
return
+ if(!user.dropItemToGround(src))
+ to_chat(user, span_warning("[src] is stuck to your hand!"))
+ return
if(flags_1 & HOLOGRAM_1)
to_chat(user, span_notice("You try to place down \the [src], but it fades away!"))
qdel(src)
return
- for(var/obj/A in T)
- if(istype(A, /obj/structure/chair))
+ for(var/obj/object in turf)
+ if(istype(object, /obj/structure/chair))
to_chat(user, span_warning("There is already a chair here!"))
return
- if(A.density && !(A.flags_1 & ON_BORDER_1))
+ if(object.density && !(object.flags_1 & ON_BORDER_1))
to_chat(user, span_warning("There is already something here!"))
return
user.visible_message(span_notice("[user] rights \the [src.name]."), span_notice("You right \the [name]."))
- var/obj/structure/chair/chair = new origin_type(get_turf(loc))
+ var/obj/structure/chair/chair = new origin_type(turf)
chair.set_custom_materials(custom_materials)
TransferComponents(chair)
chair.setDir(user.dir)
diff --git a/code/modules/antagonists/abductor/equipment/gear/abductor_items.dm b/code/modules/antagonists/abductor/equipment/gear/abductor_items.dm
index febfa8758a4..6fb876a07c2 100644
--- a/code/modules/antagonists/abductor/equipment/gear/abductor_items.dm
+++ b/code/modules/antagonists/abductor/equipment/gear/abductor_items.dm
@@ -445,7 +445,7 @@ Return to step 11 of normal process."}
span_userdanger("[user] begins shaping an energy field around your hands!"))
if(do_after(user, time_to_cuff, carbon_victim) && carbon_victim.canBeHandcuffed())
if(!carbon_victim.handcuffed)
- carbon_victim.set_handcuffed(new /obj/item/restraints/handcuffs/energy/used(carbon_victim))
+ carbon_victim.set_handcuffed(new /obj/item/restraints/handcuffs/energy(carbon_victim))
to_chat(user, span_notice("You restrain [carbon_victim]."))
log_combat(user, carbon_victim, "handcuffed")
else
@@ -481,22 +481,16 @@ Return to step 11 of normal process."}
name = "hard-light energy field"
desc = "A hard-light field restraining the hands."
icon_state = "cuff" // Needs sprite
- lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
breakouttime = 45 SECONDS
- trashtype = /obj/item/restraints/handcuffs/energy/used
flags_1 = NONE
-/obj/item/restraints/handcuffs/energy/used
- item_flags = DROPDEL
-
-/obj/item/restraints/handcuffs/energy/used/dropped(mob/user)
- user.visible_message(span_danger("[user]'s [name] breaks in a discharge of energy!"), \
- span_userdanger("[user]'s [name] breaks in a discharge of energy!"))
- var/datum/effect_system/spark_spread/sparks = new
- sparks.set_up(4,0,user.loc)
- sparks.start()
+/obj/item/restraints/handcuffs/energy/on_uncuffed(datum/source, mob/living/wearer)
. = ..()
+ wearer.visible_message(span_danger("[wearer]'s [name] breaks in a discharge of energy!"), span_userdanger("[wearer]'s [name] breaks in a discharge of energy!"))
+ var/datum/effect_system/spark_spread/sparks = new
+ sparks.set_up(4,0,wearer.loc)
+ sparks.start()
+ qdel(src)
/obj/item/melee/baton/abductor/examine(mob/user)
. = ..()
diff --git a/code/modules/antagonists/cult/blood_magic.dm b/code/modules/antagonists/cult/blood_magic.dm
index 23af33c99e9..3ff5ccc9bdd 100644
--- a/code/modules/antagonists/cult/blood_magic.dm
+++ b/code/modules/antagonists/cult/blood_magic.dm
@@ -572,7 +572,7 @@
span_userdanger("[user] begins shaping dark magic shackles around your wrists!"))
if(do_after(user, 3 SECONDS, C))
if(!C.handcuffed)
- C.set_handcuffed(new /obj/item/restraints/handcuffs/energy/cult/used(C))
+ C.equip_to_slot_or_del(new /obj/item/restraints/handcuffs/cult, ITEM_SLOT_HANDCUFFED, indirect_action = TRUE)
C.adjust_silence(10 SECONDS)
to_chat(user, span_notice("You shackle [C]."))
log_combat(user, C, "shackled")
@@ -584,19 +584,6 @@
else
to_chat(user, span_warning("[C] is already bound."))
-
-/obj/item/restraints/handcuffs/energy/cult //For the shackling spell
- name = "shadow shackles"
- desc = "Shackles that bind the wrists with sinister magic."
- trashtype = /obj/item/restraints/handcuffs/energy/used
- item_flags = DROPDEL
-
-/obj/item/restraints/handcuffs/energy/cult/used/dropped(mob/user)
- user.visible_message(span_danger("[user]'s shackles shatter in a discharge of dark magic!"), \
- span_userdanger("Your [src] shatters in a discharge of dark magic!"))
- . = ..()
-
-
//Construction: Converts 50 iron to a construct shell, plasteel to runed metal, airlock to brittle runed airlock, a borg to a construct, or borg shell to a construct shell
/obj/item/melee/blood_magic/construction
name = "Twisting Aura"
diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
index 210baad0dda..b9fcc0fe2ef 100644
--- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
+++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm
@@ -333,7 +333,7 @@
var/turf/destination = get_turf(destination_landmark)
sac_target.visible_message(span_danger("[sac_target] begins to shudder violenty as dark tendrils begin to drag them into thin air!"))
- sac_target.set_handcuffed(new /obj/item/restraints/handcuffs/energy/cult(sac_target))
+ sac_target.equip_to_slot_or_del(new /obj/item/restraints/handcuffs/cult, ITEM_SLOT_HANDCUFFED, indirect_action = TRUE)
sac_target.dropItemToGround(sac_target.legcuffed, TRUE)
sac_target.adjustOrganLoss(ORGAN_SLOT_BRAIN, 85, 150)
diff --git a/code/modules/food_and_drinks/machinery/stove.dm b/code/modules/food_and_drinks/machinery/stove.dm
index 94aa16a9da2..701c39a2dd8 100644
--- a/code/modules/food_and_drinks/machinery/stove.dm
+++ b/code/modules/food_and_drinks/machinery/stove.dm
@@ -47,6 +47,7 @@
/obj/item/reagent_containers/cup/soup_pot/Initialize(mapload, vol)
. = ..()
+ AddElement(/datum/element/cuffable_item)
RegisterSignal(src, COMSIG_ATOM_REAGENT_EXAMINE, PROC_REF(reagent_special_examine))
register_context()
diff --git a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
index 96587e0159e..6448e3062a2 100644
--- a/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
+++ b/code/modules/jobs/job_types/chaplain/chaplain_nullrod.dm
@@ -319,6 +319,10 @@
toolspeed = 0.5 //same speed as an active chainsaw
chaplain_spawnable = FALSE //prevents being pickable as a chaplain weapon (it has 30 force)
+/obj/item/nullrod/vibro/talking/chainsword/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //Thanks goodness it cannot be selected by chappies
+
/// Other Variants
/// Not a special category on their own, but usually possess more unique mechanics
diff --git a/code/modules/mining/equipment/mining_tools.dm b/code/modules/mining/equipment/mining_tools.dm
index 00620f1074d..3c24a608262 100644
--- a/code/modules/mining/equipment/mining_tools.dm
+++ b/code/modules/mining/equipment/mining_tools.dm
@@ -73,6 +73,10 @@
hitsound = 'sound/items/weapons/drill.ogg'
desc = "An electric mining drill for the especially scrawny."
+/obj/item/pickaxe/drill/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //closed handle
+
/obj/item/pickaxe/drill/diamonddrill
name = "diamond-tipped mining drill"
icon_state = "diamonddrill"
diff --git a/code/modules/mob/living/carbon/carbon_stripping.dm b/code/modules/mob/living/carbon/carbon_stripping.dm
index 054444c4815..66f2b8b6b99 100644
--- a/code/modules/mob/living/carbon/carbon_stripping.dm
+++ b/code/modules/mob/living/carbon/carbon_stripping.dm
@@ -6,14 +6,15 @@
key = STRIPPABLE_ITEM_BACK
item_slot = ITEM_SLOT_BACK
-/datum/strippable_item/mob_item_slot/back/get_alternate_actions(atom/source, mob/user)
- return get_strippable_alternate_action_internals(get_item(source), source)
+/datum/strippable_item/mob_item_slot/back/get_alternate_actions(atom/source, mob/user, obj/item/item)
+ . = ..()
+ . += get_strippable_alternate_action_internals(item, source)
-/datum/strippable_item/mob_item_slot/back/perform_alternate_action(atom/source, mob/user, action_key)
+/datum/strippable_item/mob_item_slot/back/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
if(!..())
return
- if(action_key in get_strippable_alternate_action_internals(get_item(source), source))
- strippable_alternate_action_internals(get_item(source), source, user)
+ if(action_key in get_strippable_alternate_action_internals(item, source))
+ strippable_alternate_action_internals(item, source, user)
/datum/strippable_item/mob_item_slot/mask
key = STRIPPABLE_ITEM_MASK
diff --git a/code/modules/mob/living/carbon/human/human_stripping.dm b/code/modules/mob/living/carbon/human/human_stripping.dm
index 961d1592baa..5b788c3248b 100644
--- a/code/modules/mob/living/carbon/human/human_stripping.dm
+++ b/code/modules/mob/living/carbon/human/human_stripping.dm
@@ -45,25 +45,23 @@ GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
key = STRIPPABLE_ITEM_JUMPSUIT
item_slot = ITEM_SLOT_ICLOTHING
-/datum/strippable_item/mob_item_slot/jumpsuit/get_alternate_actions(atom/source, mob/user)
- var/obj/item/clothing/under/jumpsuit = get_item(source)
+/datum/strippable_item/mob_item_slot/jumpsuit/get_alternate_actions(atom/source, mob/user, obj/item/item)
+ . = ..()
+ var/obj/item/clothing/under/jumpsuit = item
if (!istype(jumpsuit))
- return null
+ return
- var/list/actions = list()
if(jumpsuit.has_sensor == HAS_SENSORS)
- actions += "adjust_sensor"
+ . += "adjust_sensor"
if(jumpsuit.can_adjust)
- actions += "adjust_jumpsuit"
+ . += "adjust_jumpsuit"
if(length(jumpsuit.attached_accessories))
- actions += "strip_accessory"
+ . += "strip_accessory"
- return actions
-
-/datum/strippable_item/mob_item_slot/jumpsuit/perform_alternate_action(atom/source, mob/user, action_key)
+/datum/strippable_item/mob_item_slot/jumpsuit/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
if (!..())
return
- var/obj/item/clothing/under/jumpsuit = get_item(source)
+ var/obj/item/clothing/under/jumpsuit = item
if (!istype(jumpsuit))
return null
@@ -168,24 +166,25 @@ GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
key = STRIPPABLE_ITEM_FEET
item_slot = ITEM_SLOT_FEET
-/datum/strippable_item/mob_item_slot/feet/get_alternate_actions(atom/source, mob/user)
- var/obj/item/clothing/shoes/shoes = get_item(source)
+/datum/strippable_item/mob_item_slot/feet/get_alternate_actions(atom/source, mob/user, obj/item/item)
+ . = ..()
+ var/obj/item/clothing/shoes/shoes = item
if (!istype(shoes) || shoes.fastening_type == SHOES_SLIPON)
- return null
+ return
switch (shoes.tied)
if (SHOES_UNTIED)
- return list("knot")
+ . += "knot"
if (SHOES_TIED)
- return list("untie")
+ . += "untie"
if (SHOES_KNOTTED)
- return list("unknot")
+ . += "unknot"
-/datum/strippable_item/mob_item_slot/feet/perform_alternate_action(atom/source, mob/user, action_key)
+/datum/strippable_item/mob_item_slot/feet/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
if(!..())
return
- var/obj/item/clothing/shoes/shoes = get_item(source)
+ var/obj/item/clothing/shoes/shoes = item
if (!istype(shoes))
return
switch(action_key)
@@ -198,14 +197,15 @@ GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
key = STRIPPABLE_ITEM_SUIT_STORAGE
item_slot = ITEM_SLOT_SUITSTORE
-/datum/strippable_item/mob_item_slot/suit_storage/get_alternate_actions(atom/source, mob/user)
- return get_strippable_alternate_action_internals(get_item(source), source)
+/datum/strippable_item/mob_item_slot/suit_storage/get_alternate_actions(atom/source, mob/user, obj/item/item)
+ . = ..()
+ . += get_strippable_alternate_action_internals(item, source)
-/datum/strippable_item/mob_item_slot/suit_storage/perform_alternate_action(atom/source, mob/user, action_key)
+/datum/strippable_item/mob_item_slot/suit_storage/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
if(!..())
return
- if(action_key in get_strippable_alternate_action_internals(get_item(source), source))
- strippable_alternate_action_internals(get_item(source), source, user)
+ if(action_key in get_strippable_alternate_action_internals(item, source))
+ strippable_alternate_action_internals(item, source, user)
/datum/strippable_item/mob_item_slot/id
key = STRIPPABLE_ITEM_ID
@@ -215,14 +215,15 @@ GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
key = STRIPPABLE_ITEM_BELT
item_slot = ITEM_SLOT_BELT
-/datum/strippable_item/mob_item_slot/belt/get_alternate_actions(atom/source, mob/user)
- return get_strippable_alternate_action_internals(get_item(source), source)
+/datum/strippable_item/mob_item_slot/belt/get_alternate_actions(atom/source, mob/user, obj/item/item)
+ . = ..()
+ . += get_strippable_alternate_action_internals(item, source)
-/datum/strippable_item/mob_item_slot/belt/perform_alternate_action(atom/source, mob/user, action_key)
+/datum/strippable_item/mob_item_slot/belt/perform_alternate_action(atom/source, mob/user, action_key, obj/item/item)
if (!..())
return
- if(action_key in get_strippable_alternate_action_internals(get_item(source), source))
- strippable_alternate_action_internals(get_item(source), source, user)
+ if(action_key in get_strippable_alternate_action_internals(item, source))
+ strippable_alternate_action_internals(item, source, user)
/datum/strippable_item/mob_item_slot/pocket
/// Which pocket we're referencing. Used for visible text.
diff --git a/code/modules/reagents/reagent_containers/cups/_cup.dm b/code/modules/reagents/reagent_containers/cups/_cup.dm
index b1ee274a09c..4fa2507864a 100644
--- a/code/modules/reagents/reagent_containers/cups/_cup.dm
+++ b/code/modules/reagents/reagent_containers/cups/_cup.dm
@@ -331,6 +331,10 @@
ITEM_SLOT_DEX_STORAGE
)
+/obj/item/reagent_containers/cup/bucket/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/datum/armor/cup_bucket
melee = 10
fire = 75
diff --git a/code/modules/reagents/reagent_containers/cups/drinks.dm b/code/modules/reagents/reagent_containers/cups/drinks.dm
index 3ab16562102..45be3585b38 100644
--- a/code/modules/reagents/reagent_containers/cups/drinks.dm
+++ b/code/modules/reagents/reagent_containers/cups/drinks.dm
@@ -71,6 +71,10 @@
custom_materials = list(/datum/material/gold=HALF_SHEET_MATERIAL_AMOUNT)
volume = 150
+/obj/item/reagent_containers/cup/glass/trophy/gold_cup/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //closed handles
+
/obj/item/reagent_containers/cup/glass/trophy/silver_cup
name = "silver cup"
desc = "Best loser!"
@@ -83,6 +87,9 @@
custom_materials = list(/datum/material/silver=SMALL_MATERIAL_AMOUNT*8)
volume = 100
+/obj/item/reagent_containers/cup/glass/trophy/silver_cup/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item) //closed handle
/obj/item/reagent_containers/cup/glass/trophy/bronze_cup
name = "bronze cup"
@@ -167,6 +174,10 @@
base_icon_state = "tea"
inhand_icon_state = "coffee"
+/obj/item/reagent_containers/cup/glass/mug/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+
/obj/item/reagent_containers/cup/glass/mug/update_icon_state()
icon_state = "[base_icon_state][reagents.total_volume ? null : "_empty"]"
return ..()
diff --git a/code/modules/reagents/reagent_containers/cups/mauna_mug.dm b/code/modules/reagents/reagent_containers/cups/mauna_mug.dm
index 569cf815f09..dcee96829dd 100644
--- a/code/modules/reagents/reagent_containers/cups/mauna_mug.dm
+++ b/code/modules/reagents/reagent_containers/cups/mauna_mug.dm
@@ -13,6 +13,7 @@
/obj/item/reagent_containers/cup/maunamug/Initialize(mapload, vol)
. = ..()
+ AddElement(/datum/element/cuffable_item)
cell = new /obj/item/stock_parts/power_store/cell(src)
/obj/item/reagent_containers/cup/maunamug/get_cell()
diff --git a/code/modules/reagents/reagent_containers/jerrycan.dm b/code/modules/reagents/reagent_containers/jerrycan.dm
index 05f3149ae54..2d8ff258340 100644
--- a/code/modules/reagents/reagent_containers/jerrycan.dm
+++ b/code/modules/reagents/reagent_containers/jerrycan.dm
@@ -67,6 +67,11 @@
///You can use this var to tone down the strength of the highlight for less shiny types of plastic.
var/highlight_strenght = 1.0
+/obj/item/reagent_containers/cup/jerrycan/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/cuffable_item)
+ update_appearance()
+
/obj/item/reagent_containers/cup/jerrycan/update_overlays()
. = ..()
@@ -91,10 +96,6 @@
if(cap_type)
. += mutable_appearance(icon_file, "[base_icon_state]_cap_[cap_type]")
-/obj/item/reagent_containers/cup/jerrycan/Initialize(mapload)
- . = ..()
- update_appearance()
-
/obj/item/reagent_containers/cup/jerrycan/opaque
fill_icon_thresholds = null
initial_reagent_flags = parent_type::initial_reagent_flags & ~TRANSPARENT
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index 58aa910cb22..ffe69b9acf4 100644
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/icons/obj/weapons/restraints.dmi b/icons/obj/weapons/restraints.dmi
index f2d2f305d68..ddd3bf96436 100644
Binary files a/icons/obj/weapons/restraints.dmi and b/icons/obj/weapons/restraints.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index e64c431926f..31c0f5220fd 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1518,6 +1518,7 @@
#include "code\datums\elements\corrupted_organ.dm"
#include "code\datums\elements\crackable.dm"
#include "code\datums\elements\crusher_loot.dm"
+#include "code\datums\elements\cuffable_item.dm"
#include "code\datums\elements\cuffsnapping.dm"
#include "code\datums\elements\cult_eyes.dm"
#include "code\datums\elements\cult_halo.dm"
@@ -1967,6 +1968,7 @@
#include "code\datums\status_effects\_status_effect_helpers.dm"
#include "code\datums\status_effects\agent_pinpointer.dm"
#include "code\datums\status_effects\buffs.dm"
+#include "code\datums\status_effects\cuffed_item.dm"
#include "code\datums\status_effects\death_sound.dm"
#include "code\datums\status_effects\drug_effects.dm"
#include "code\datums\status_effects\gas.dm"
diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx
index bf6b255aa32..d77698d65e6 100644
--- a/tgui/packages/tgui/interfaces/StripMenu.tsx
+++ b/tgui/packages/tgui/interfaces/StripMenu.tsx
@@ -59,6 +59,11 @@ const ALTERNATE_ACTIONS: Record = {
text: 'Unknot',
},
+ remove_item_cuffs: {
+ icon: 'handcuffs',
+ text: 'Remove Handcuffs',
+ },
+
enable_internals: {
icon: 'tg-air-tank',
text: 'Enable internals',