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:
Ghom
2025-10-07 13:24:20 +02:00
committed by GitHub
parent 76129b9fb3
commit f72eb75a6f
40 changed files with 591 additions and 193 deletions

View File

@@ -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(): ()

View File

@@ -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)

View File

@@ -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"

View File

@@ -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),
)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 <a href='byond://?src=[REF(item)];remove_cuffs_item=1'>remove them</a>.")
/// 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)

View File

@@ -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.

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -7,3 +7,6 @@
resistance_flags = FLAMMABLE
storage_type = /datum/storage/basket
/obj/item/storage/basket/Initialize(mapload)
. = ..()
AddElement(/datum/element/cuffable_item)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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."

View File

@@ -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()))

View File

@@ -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, \

View File

@@ -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)

View File

@@ -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)
. = ..()

View File

@@ -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"

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 ..()

View File

@@ -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()

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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"

View File

@@ -59,6 +59,11 @@ const ALTERNATE_ACTIONS: Record<string, AlternateAction> = {
text: 'Unknot',
},
remove_item_cuffs: {
icon: 'handcuffs',
text: 'Remove Handcuffs',
},
enable_internals: {
icon: 'tg-air-tank',
text: 'Enable internals',