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',