diff --git a/code/__DEFINES/strippable.dm b/code/__DEFINES/strippable.dm
new file mode 100644
index 000000000000..f62c4d6c1b76
--- /dev/null
+++ b/code/__DEFINES/strippable.dm
@@ -0,0 +1,30 @@
+// All of these must be matched in StripMenu.js.
+#define STRIPPABLE_ITEM_HEAD "head"
+#define STRIPPABLE_ITEM_BACK "back"
+#define STRIPPABLE_ITEM_MASK "wear_mask"
+#define STRIPPABLE_ITEM_EYES "glasses"
+#define STRIPPABLE_ITEM_L_EAR "wear_l_ear"
+#define STRIPPABLE_ITEM_R_EAR "wear_r_ear"
+#define STRIPPABLE_ITEM_JUMPSUIT "w_uniform"
+#define STRIPPABLE_ITEM_SUIT "wear_suit"
+#define STRIPPABLE_ITEM_GLOVES "gloves"
+#define STRIPPABLE_ITEM_FEET "shoes"
+#define STRIPPABLE_ITEM_SUIT_STORAGE "j_store"
+#define STRIPPABLE_ITEM_ID "id"
+#define STRIPPABLE_ITEM_BELT "belt"
+#define STRIPPABLE_ITEM_LPOCKET "l_store"
+#define STRIPPABLE_ITEM_RPOCKET "r_store"
+#define STRIPPABLE_ITEM_LHAND "l_hand"
+#define STRIPPABLE_ITEM_RHAND "r_hand"
+#define STRIPPABLE_ITEM_HANDCUFFS "handcuffs"
+#define STRIPPABLE_ITEM_LEGCUFFS "legcuffs"
+
+
+/// This slot is not obscured.
+#define STRIPPABLE_OBSCURING_NONE 0
+
+/// This slot is completely obscured, and cannot be accessed.
+#define STRIPPABLE_OBSCURING_COMPLETELY 1
+
+/// This slot can't be seen, but can be accessed.
+#define STRIPPABLE_OBSCURING_HIDDEN 2
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index fe15e6d84c79..d5bc76933230 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -176,3 +176,10 @@
for(var/i in 1 to inserted_list.len - 1)
inserted_list.Swap(i, rand(i, inserted_list.len))
+
+/// Performs an insertion on the given lazy list with the given key and value. If the value already exists, a new one will not be made.
+#define LAZYORASSOCLIST(lazy_list, key, value) \
+ LAZYINITLIST(lazy_list); \
+ LAZYINITLIST(lazy_list[key]); \
+ lazy_list[key] |= value;
+
diff --git a/code/_onclick/click_hold.dm b/code/_onclick/click_hold.dm
index 2a766580e366..41e2be147d85 100644
--- a/code/_onclick/click_hold.dm
+++ b/code/_onclick/click_hold.dm
@@ -97,7 +97,6 @@
/client/MouseDrop(datum/src_object, datum/over_object, src_location, over_location, src_control, over_control, params)
. = ..()
-
if(over_object)
SEND_SIGNAL(over_object, COMSIG_ATOM_DROPPED_ON, src_object, src)
diff --git a/code/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm
index fff5e9200de7..4dcc0d646816 100644
--- a/code/_onclick/drag_drop.dm
+++ b/code/_onclick/drag_drop.dm
@@ -7,6 +7,7 @@
*/
/atom/MouseDrop(atom/over)
if(!usr || !over) return
+
if(!Adjacent(usr) || !over.Adjacent(usr)) return // should stop you from dragging through windows
spawn(0)
diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm
new file mode 100644
index 000000000000..2300e1bc3f0a
--- /dev/null
+++ b/code/datums/elements/strippable.dm
@@ -0,0 +1,536 @@
+/// An element for atoms that, when dragged and dropped onto a mob, opens a strip panel.
+/datum/element/strippable
+ element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
+ id_arg_index = 2
+
+ /// An assoc list of keys to /datum/strippable_item
+ var/list/items
+
+ /// A proc path that returns TRUE/FALSE if we should show the strip panel for this entity.
+ /// If it does not exist, the strip menu will always show.
+ /// Will be called with (mob/user).
+ var/should_strip_proc_path
+
+ /// An existing strip menus
+ var/list/strip_menus
+
+/datum/element/strippable/Attach(datum/target, list/items, should_strip_proc_path)
+ . = ..()
+ if (!isatom(target))
+ return ELEMENT_INCOMPATIBLE
+
+ RegisterSignal(target, COMSIG_ATOM_DROP_ON, PROC_REF(mouse_drop_onto))
+
+ src.items = items
+ src.should_strip_proc_path = should_strip_proc_path
+
+/datum/element/strippable/Detach(datum/source, force)
+ . = ..()
+
+ UnregisterSignal(source, COMSIG_ATOM_DROP_ON)
+
+ if (!isnull(strip_menus))
+ QDEL_NULL(strip_menus[source])
+
+/datum/element/strippable/proc/mouse_drop_onto(datum/source, atom/over, mob/user)
+ SIGNAL_HANDLER
+ if (user == source)
+ return
+
+ if (over == source)
+ return
+
+ var/mob/overmob = over
+ if (!ishuman(overmob))
+ return
+
+ if (!overmob.Adjacent(source))
+ return
+
+ if (!overmob.client)
+ return
+
+ if (overmob.client != user)
+ return
+
+ if (!isnull(should_strip_proc_path) && !call(source, should_strip_proc_path)(overmob))
+ return
+
+ var/datum/strip_menu/strip_menu
+
+ if (isnull(strip_menu))
+ strip_menu = new(source, src)
+ LAZYSET(strip_menus, source, strip_menu)
+
+ INVOKE_ASYNC(strip_menu, PROC_REF(tgui_interact), overmob)
+
+/// A representation of an item that can be stripped down
+/datum/strippable_item
+ /// The STRIPPABLE_ITEM_* key
+ var/key
+
+ /// Should we warn about dangerous clothing?
+ var/warn_dangerous_clothing = TRUE
+
+/// Gets the item from the given source.
+/datum/strippable_item/proc/get_item(atom/source)
+
+/// Tries to equip the item onto the given source.
+/// Returns TRUE/FALSE depending on if it is allowed.
+/// This should be used for checking if an item CAN be equipped.
+/// It should not perform the equipping itself.
+/datum/strippable_item/proc/try_equip(atom/source, obj/item/equipping, mob/user)
+ if ((equipping.flags_item & ITEM_ABSTRACT))
+ return FALSE
+ if ((equipping.flags_item & NODROP))
+ to_chat(user, SPAN_WARNING("You can't put [equipping] on [source], it's stuck to your hand!"))
+ return FALSE
+ if (ishuman(source))
+ var/mob/living/carbon/human/sourcehuman = source
+ if(HAS_TRAIT(sourcehuman, TRAIT_UNSTRIPPABLE) && !sourcehuman.is_mob_incapacitated())
+ to_chat(src, SPAN_DANGER("[sourcehuman] is too strong to force [equipping] onto them!"))
+ return
+ return TRUE
+
+/// Start the equipping process. This is the proc you should yield in.
+/// Returns TRUE/FALSE depending on if it is allowed.
+/datum/strippable_item/proc/start_equip(atom/source, obj/item/equipping, mob/user)
+ source.visible_message(
+ SPAN_NOTICE("[user] tries to put [equipping] on [source]."),
+ SPAN_NOTICE("[user] tries to put [equipping] on you.")
+ )
+
+ if (ismob(source))
+ var/mob/sourcemob = source
+ sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] is having [equipping] put on them by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] [key_name(user)] is putting [equipping] on [key_name(sourcemob)]")
+
+ return TRUE
+
+/// The proc that places the item on the source. This should not yield.
+/datum/strippable_item/proc/finish_equip(atom/source, obj/item/equipping, mob/user)
+ SHOULD_NOT_SLEEP(TRUE)
+
+/// Tries to unequip the item from the given source.
+/// Returns TRUE/FALSE depending on if it is allowed.
+/// This should be used for checking if it CAN be unequipped.
+/// It should not perform the unequipping itself.
+/datum/strippable_item/proc/try_unequip(atom/source, mob/user)
+ SHOULD_NOT_SLEEP(TRUE)
+
+ var/obj/item/item = get_item(source)
+ if (isnull(item))
+ return FALSE
+
+ if (user.action_busy && !skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED))
+ to_chat(user, SPAN_WARNING("You can't do this right now."))
+ return FALSE
+
+ if ((item.flags_inventory & CANTSTRIP) || (item.flags_item & NODROP) || (item.flags_item & ITEM_ABSTRACT))
+ return FALSE
+
+ if (ishuman(source))
+ var/mob/living/carbon/human/sourcehuman = source
+ if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcehuman.stat == DEAD || sourcehuman.health < HEALTH_THRESHOLD_CRIT) && !sourcehuman.get_target_lock(user.faction_group))
+ to_chat(user, SPAN_WARNING("You can't strip items of a crit or dead member of another faction!"))
+ return FALSE
+
+ if(HAS_TRAIT(sourcehuman, TRAIT_UNSTRIPPABLE) && !sourcehuman.is_mob_incapacitated())
+ to_chat(src, SPAN_DANGER("[sourcehuman] has an unbreakable grip on their equipment!"))
+ return
+
+ return TRUE
+
+/// Start the unequipping process. This is the proc you should yield in.
+/// Returns TRUE/FALSE depending on if it is allowed.
+/datum/strippable_item/proc/start_unequip(atom/source, mob/user)
+ var/obj/item/item = get_item(source)
+ if (isnull(item))
+ return FALSE
+
+ source.visible_message(
+ SPAN_WARNING("[user] tries to remove [source]'s [item]."),
+ SPAN_DANGER("[user] tries to remove your [item].")
+ )
+
+ if (ismob(source))
+ var/mob/sourcemob = source
+ sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] is being stripped of [item] by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] [key_name(user)] is stripping [key_name(sourcemob)] of [item]")
+
+ item.add_fingerprint(user)
+
+ return TRUE
+
+/// The proc that unequips the item from the source. This should not yield.
+/datum/strippable_item/proc/finish_unequip(atom/source, mob/user)
+
+/// Returns a STRIPPABLE_OBSCURING_* define to report on whether or not this is obscured.
+/datum/strippable_item/proc/get_obscuring(atom/source)
+ SHOULD_NOT_SLEEP(TRUE)
+ return STRIPPABLE_OBSCURING_NONE
+
+/// Returns the ID of this item's strippable action.
+/// Return `null` if there is no alternate action.
+/// Any return value of this must be in StripMenu.
+/datum/strippable_item/proc/get_alternate_action(atom/source, mob/user)
+ return null
+
+/// Performs an alternative action on this strippable_item.
+/// `has_alternate_action` needs to be TRUE.
+/datum/strippable_item/proc/alternate_action(atom/source, mob/user)
+
+/// Returns whether or not this item should show.
+/datum/strippable_item/proc/should_show(atom/source, mob/user)
+ return TRUE
+
+/// A preset for equipping items onto mob slots
+/datum/strippable_item/mob_item_slot
+ /// The ITEM_SLOT_* to equip to.
+ var/item_slot
+
+/datum/strippable_item/proc/has_no_item_alt_action()
+ return FALSE
+
+/datum/strippable_item/mob_item_slot/get_item(atom/source)
+ if (!ismob(source))
+ return null
+
+ var/mob/mob_source = source
+ return mob_source.get_item_by_slot(key)
+
+/datum/strippable_item/mob_item_slot/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ if (!ismob(source))
+ return FALSE
+ if (user.action_busy)
+ to_chat(user, SPAN_WARNING("You can't do this right now."))
+ return FALSE
+ if (!equipping.mob_can_equip(
+ source,
+ key
+ ))
+ to_chat(user, SPAN_WARNING("\The [equipping] doesn't fit in that place!"))
+ return FALSE
+ if(equipping.flags_item & WIELDED)
+ equipping.unwield(user)
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/start_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ if (!ismob(source))
+ return FALSE
+
+ var/time_to_strip = HUMAN_STRIP_DELAY
+ var/mob/sourcemob = source
+
+ if (ishuman(sourcemob) && ishuman(user))
+ var/mob/living/carbon/human/sourcehuman = sourcemob
+ var/mob/living/carbon/human/userhuman = user
+ time_to_strip = userhuman.get_strip_delay(userhuman, sourcehuman)
+
+ if (!do_after(user, time_to_strip, INTERRUPT_ALL, BUSY_ICON_FRIENDLY, source, INTERRUPT_MOVED, BUSY_ICON_FRIENDLY))
+ return FALSE
+
+ if (!equipping.mob_can_equip(
+ sourcemob,
+ key
+ ))
+ return FALSE
+
+ if (!user.temp_drop_inv_item(equipping))
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/finish_equip(atom/source, obj/item/equipping, mob/user)
+ if (!ismob(source))
+ return FALSE
+
+ var/mob/sourcemob = source
+ sourcemob.equip_to_slot_if_possible(equipping, key)
+
+/datum/strippable_item/mob_item_slot/get_obscuring(atom/source)
+ return FALSE
+
+/datum/strippable_item/mob_item_slot/start_unequip(atom/source, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ return start_unequip_mob(get_item(source), source, user)
+
+/datum/strippable_item/mob_item_slot/finish_unequip(atom/source, mob/user)
+ var/obj/item/item = get_item(source)
+ if (isnull(item))
+ return FALSE
+
+ if (!ismob(source))
+ return FALSE
+
+ return finish_unequip_mob(item, source, user)
+
+/// A utility function for `/datum/strippable_item`s to start unequipping an item from a mob.
+/datum/strippable_item/mob_item_slot/proc/start_unequip_mob(obj/item/item, mob/living/carbon/human/source, mob/living/carbon/human/user)
+ var/time_to_strip = HUMAN_STRIP_DELAY
+
+ if (istype(source) && istype(user))
+ time_to_strip = user.get_strip_delay(user, source)
+
+ if (!do_after(user, time_to_strip, INTERRUPT_ALL, BUSY_ICON_HOSTILE, source, INTERRUPT_MOVED, BUSY_ICON_HOSTILE))
+ return FALSE
+
+ return TRUE
+
+/// A utility function for `/datum/strippable_item`s to finish unequipping an item from a mob.
+/datum/strippable_item/mob_item_slot/proc/finish_unequip_mob(obj/item/item, mob/source, mob/user)
+ if (!source.drop_inv_item_on_ground(item))
+ return FALSE
+
+ if (ismob(source))
+ var/mob/sourcemob = source
+ sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] has been stripped of [item] by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] [key_name(user)] has been stripped of [key_name(sourcemob)] of [item]")
+
+ // Updates speed in case stripped speed affecting item
+ source.recalculate_move_delay = TRUE
+
+/// A representation of the stripping UI
+/datum/strip_menu
+ /// The owner who has the element /datum/element/strippable
+ var/atom/movable/owner
+
+ /// The strippable element itself
+ var/datum/element/strippable/strippable
+
+ /// A lazy list of user mobs to a list of strip menu keys that they're interacting with
+ var/list/interactions
+
+/datum/strip_menu/New(atom/movable/owner, datum/element/strippable/strippable)
+ . = ..()
+ src.owner = owner
+ src.strippable = strippable
+
+/datum/strip_menu/Destroy()
+ owner = null
+ strippable = null
+
+ return ..()
+
+/datum/strip_menu/tgui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if (!ui)
+ ui = new(user, src, "StripMenu")
+ ui.open()
+
+
+/datum/strip_menu/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/simple/inventory),
+ )
+
+/datum/strip_menu/ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/items = list()
+
+ for (var/strippable_key in strippable.items)
+ var/datum/strippable_item/item_data = strippable.items[strippable_key]
+
+ if (!item_data.should_show(owner, user))
+ continue
+
+ var/list/result
+
+ if(strippable_key in LAZYACCESS(interactions, user))
+ LAZYSET(result, "interacting", TRUE)
+
+ var/obscuring = item_data.get_obscuring(owner)
+ if (obscuring != STRIPPABLE_OBSCURING_NONE)
+ LAZYSET(result, "obscured", obscuring)
+ items[strippable_key] = result
+ continue
+
+ var/obj/item/item = item_data.get_item(owner)
+ if (isnull(item))
+ if (item_data.has_no_item_alt_action())
+ LAZYINITLIST(result)
+ result["no_item_action"] = item_data.get_alternate_action(owner, user)
+ items[strippable_key] = result
+ continue
+
+ LAZYINITLIST(result)
+
+ result["icon"] = icon2base64(icon(item.icon, item.icon_state, frame = 1))
+ result["name"] = item.name
+ result["alternate"] = item_data.get_alternate_action(owner, user)
+
+ items[strippable_key] = result
+
+ data["items"] = items
+
+ // While most `\the`s are implicit, this one is not.
+ // In this case, `\The` would otherwise be used.
+ // This doesn't match with what it's used for, which is to say "Stripping the alien drone",
+ // as opposed to "Stripping The alien drone".
+ // Human names will still show without "the", as they are proper nouns.
+ data["name"] = "\the [owner]"
+
+ return data
+
+/datum/strip_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if (.)
+ return
+
+ . = TRUE
+
+ var/mob/user = ui.user
+
+ switch (action)
+ if ("equip")
+ var/key = params["key"]
+ var/datum/strippable_item/strippable_item = strippable.items[key]
+
+ if (isnull(strippable_item))
+ return
+
+ if (!strippable_item.should_show(owner, user))
+ return
+
+ if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY)
+ return
+
+ var/item = strippable_item.get_item(owner)
+ if (!isnull(item))
+ return
+
+ var/obj/item/held_item = user.get_held_item()
+ if (isnull(held_item))
+ return
+
+ if (!strippable_item.try_equip(owner, held_item, user))
+ return
+
+ LAZYORASSOCLIST(interactions, user, key)
+
+ // Yielding call
+ var/should_finish = strippable_item.start_equip(owner, held_item, user)
+
+ LAZYREMOVEASSOC(interactions, user, key)
+
+ if (!should_finish)
+ return
+
+ if (QDELETED(src) || QDELETED(owner))
+ return
+
+ // They equipped an item in the meantime
+ if (!isnull(strippable_item.get_item(owner)))
+ return
+
+ if (!user.Adjacent(owner))
+ return
+
+ strippable_item.finish_equip(owner, held_item, user)
+ if ("strip")
+ var/key = params["key"]
+ var/datum/strippable_item/strippable_item = strippable.items[key]
+
+ if (isnull(strippable_item))
+ return
+
+ if (!strippable_item.should_show(owner, user))
+ return
+
+ if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY)
+ return
+
+ var/item = strippable_item.get_item(owner)
+ if (isnull(item))
+ return
+
+ if (!strippable_item.try_unequip(owner, user))
+ return
+
+ LAZYORASSOCLIST(interactions, user, key)
+
+ var/should_unequip = strippable_item.start_unequip(owner, user)
+
+ LAZYREMOVEASSOC(interactions, user, key)
+
+ // Yielding call
+ if (!should_unequip)
+ return
+
+ if (QDELETED(src) || QDELETED(owner))
+ return
+
+ // They changed the item in the meantime
+ if (strippable_item.get_item(owner) != item)
+ return
+
+ if (!user.Adjacent(owner))
+ return
+
+ strippable_item.finish_unequip(owner, user)
+ if ("alt")
+ var/key = params["key"]
+ var/datum/strippable_item/strippable_item = strippable.items[key]
+
+ if (isnull(strippable_item))
+ return
+
+ if (!strippable_item.should_show(owner, user))
+ return
+
+ if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY)
+ return
+
+ var/item = strippable_item.get_item(owner)
+ if (isnull(item) && !strippable_item.has_no_item_alt_action())
+ return
+
+ if (isnull(strippable_item.get_alternate_action(owner, user)))
+ return
+
+ LAZYORASSOCLIST(interactions, user, key)
+
+ // Potentially yielding
+ strippable_item.alternate_action(owner, user)
+
+ LAZYREMOVEASSOC(interactions, user, key)
+
+/datum/strip_menu/ui_host(mob/user)
+ return owner
+
+/datum/strip_menu/ui_status(mob/user, datum/ui_state/state)
+ . = ..()
+
+ if (isliving(user))
+ var/mob/living/living_user = user
+
+ if (
+ . == UI_UPDATE \
+ && user.stat == CONSCIOUS \
+ && living_user.body_position == LYING_DOWN \
+ && user.Adjacent(owner)
+ )
+ return UI_INTERACTIVE
+
+/// Creates an assoc list of keys to /datum/strippable_item
+/proc/create_strippable_list(types)
+ var/list/strippable_items = list()
+
+ for (var/strippable_type in types)
+ var/datum/strippable_item/strippable_item = new strippable_type
+ strippable_items[strippable_item.key] = strippable_item
+
+ return strippable_items
diff --git a/code/game/objects/items/weapons/twohanded.dm b/code/game/objects/items/weapons/twohanded.dm
index be7571fa84a1..36e0ea702a95 100644
--- a/code/game/objects/items/weapons/twohanded.dm
+++ b/code/game/objects/items/weapons/twohanded.dm
@@ -101,7 +101,7 @@
w_class = SIZE_HUGE
icon_state = "offhand"
name = "offhand"
- flags_item = DELONDROP|TWOHANDED|WIELDED
+ flags_item = DELONDROP|TWOHANDED|WIELDED|CANTSTRIP
/obj/item/weapon/twohanded/offhand/unwield(mob/user)
if(flags_item & WIELDED)
diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm
index 8e19a1905300..1d88df0b6a6b 100644
--- a/code/modules/asset_cache/asset_list.dm
+++ b/code/modules/asset_cache/asset_list.dm
@@ -341,3 +341,25 @@ GLOBAL_LIST_EMPTY(asset_datums)
if (!item_filename)
return
. = list("[item_filename]" = SSassets.transport.get_asset_url(item_filename))
+
+/datum/asset/simple/inventory
+ assets = list(
+ "inventory-glasses.png" = 'icons/ui_Icons/inventory/glasses.png',
+ "inventory-head.png" = 'icons/ui_Icons/inventory/head.png',
+ "inventory-neck.png" = 'icons/ui_Icons/inventory/neck.png',
+ "inventory-mask.png" = 'icons/ui_Icons/inventory/mask.png',
+ "inventory-ears.png" = 'icons/ui_Icons/inventory/ears.png',
+ "inventory-uniform.png" = 'icons/ui_Icons/inventory/uniform.png',
+ "inventory-suit.png" = 'icons/ui_Icons/inventory/suit.png',
+ "inventory-gloves.png" = 'icons/ui_Icons/inventory/gloves.png',
+ "inventory-hand_l.png" = 'icons/ui_Icons/inventory/hand_l.png',
+ "inventory-hand_r.png" = 'icons/ui_Icons/inventory/hand_r.png',
+ "inventory-shoes.png" = 'icons/ui_Icons/inventory/shoes.png',
+ "inventory-suit_storage.png" = 'icons/ui_Icons/inventory/suit_storage.png',
+ "inventory-id.png" = 'icons/ui_Icons/inventory/id.png',
+ "inventory-belt.png" = 'icons/ui_Icons/inventory/belt.png',
+ "inventory-back.png" = 'icons/ui_Icons/inventory/back.png',
+ "inventory-pocket.png" = 'icons/ui_Icons/inventory/pocket.png',
+ "inventory-collar.png" = 'icons/ui_Icons/inventory/collar.png',
+ )
+
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 08daa5348022..2a197dadc2b1 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -399,23 +399,6 @@
bodytemperature = max(bodytemperature, BODYTEMP_HEAT_DAMAGE_LIMIT+10)
recalculate_move_delay = TRUE
-
-/mob/living/carbon/show_inv(mob/living/carbon/user as mob)
- user.set_interaction(src)
- var/dat = {"
-
[name]
-
-
Head(Mask): [(wear_mask ? wear_mask : "Nothing")]
-
Left Hand: [(l_hand ? l_hand : "Nothing")]
-
Right Hand: [(r_hand ? r_hand : "Nothing")]
-
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? " Set Internal" : "")]
-
[(handcuffed ? "Handcuffed" : "Not Handcuffed")]
-
[(internal ? "Remove Internal" : "")]
-
Refresh
-
Close
-
"}
- show_browser(user, dat, name, "mob[name]")
-
/**
* Called by [/mob/dead/observer/proc/do_observe] when a carbon mob is observed by a ghost with [/datum/preferences/var/auto_observe] enabled.
*
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 0f52b75106a5..4e71b178824d 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -13,7 +13,7 @@
create_reagents(1000)
if(!real_name || !name)
change_real_name(src, "unknown")
-
+ AddElement(/datum/element/strippable, GLOB.strippable_human_items, TYPE_PROC_REF(/mob/living/carbon/human, should_strip))
. = ..()
prev_gender = gender // Debug for plural genders
@@ -272,45 +272,6 @@
-/mob/living/carbon/human/show_inv(mob/living/user)
- var/obj/item/clothing/under/suit = null
- if(istype(w_uniform, /obj/item/clothing/under))
- suit = w_uniform
-
- user.set_interaction(src)
- var/dat = {"
-
[name]
-
-
(Exo)Suit: [(wear_suit ? wear_suit : "Nothing")]
-
Suit Storage: [(s_store ? s_store : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(s_store, /obj/item/tank) && !( internal )) ? " Set Internal" : "")]
-
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? " Set Internal" : "")]
-
Head(Mask): [(wear_mask ? wear_mask : "Nothing")]
-
Left Hand: [(l_hand ? l_hand : "Nothing")]
-
Right Hand: [(r_hand ? r_hand : "Nothing")]
-
Gloves: [(gloves ? gloves : "Nothing")]
-
Eyes: [(glasses ? glasses : "Nothing")]
-
Left Ear: [(wear_l_ear ? wear_l_ear : "Nothing")]
-
Right Ear: [(wear_r_ear ? wear_r_ear : "Nothing")]
-
Head: [(head ? head : "Nothing")]
-
Shoes: [(shoes ? shoes : "Nothing")]
-
Belt: [(belt ? belt : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(belt, /obj/item/tank) && !internal) ? " Set Internal" : "")]
-
Uniform: [(w_uniform ? w_uniform : "Nothing")] [(suit) ? ((suit.has_sensor == UNIFORM_HAS_SENSORS) ? " Sensors" : "") : null]
-
ID: [(wear_id ? wear_id : "Nothing")]
-
Left Pocket: [(l_store ? l_store : "Nothing")]
-
Right Pocket: [(r_store ? r_store : "Nothing")]
-
- [handcuffed ? "
Handcuffed" : ""]
- [legcuffed ? "
Legcuffed" : ""]
- [suit && LAZYLEN(suit.accessories) ? "
Remove Accessory" : ""]
- [internal ? "
Remove Internal" : ""]
- [istype(wear_id, /obj/item/card/id/dogtag) ? "
Retrieve Info Tag" : ""]
-
Remove Splints
-
-
Refresh
-
Close
-
"}
- show_browser(user, dat, name, "mob[name]")
-
/**
* Handles any storage containers that the human is looking inside when auto-observed.
*/
@@ -426,9 +387,6 @@
/mob/living/carbon/human/Topic(href, href_list)
- if(href_list["refresh"])
- if(interactee&&(in_range(src, usr)))
- show_inv(interactee)
if(href_list["mach_close"])
var/t1 = text("window=[]", href_list["mach_close"])
@@ -473,76 +431,6 @@
what = usr.get_active_hand()
usr.stripPanelEquip(what,src,slot)
- if(href_list["internal"])
-
- if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr))
- attack_log += text("\[[time_stamp()]\] Has had their internals toggled by [key_name(usr)]")
- usr.attack_log += text("\[[time_stamp()]\] Attempted to toggle [key_name(src)]'s' internals")
- if(internal)
- usr.visible_message(SPAN_DANGER("[usr] is trying to disable [src]'s internals"), null, null, 3)
- else
- usr.visible_message(SPAN_DANGER("[usr] is trying to enable [src]'s internals."), null, null, 3)
-
- if(do_after(usr, POCKET_STRIP_DELAY, INTERRUPT_ALL, BUSY_ICON_GENERIC, src, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
- if(internal)
- internal.add_fingerprint(usr)
- internal = null
- visible_message("[src] is no longer running on internals.", null, null, 1)
- else
- if(istype(wear_mask, /obj/item/clothing/mask))
- if(istype(back, /obj/item/tank))
- internal = back
- else if(istype(s_store, /obj/item/tank))
- internal = s_store
- else if(istype(belt, /obj/item/tank))
- internal = belt
- if(internal)
- visible_message(SPAN_NOTICE("[src] is now running on internals."), null, null, 1)
- internal.add_fingerprint(usr)
-
- // Update strip window
- if(usr.interactee == src && Adjacent(usr))
- show_inv(usr)
-
-
- if(href_list["splints"])
- if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr))
- if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group))
- to_chat(usr, SPAN_WARNING("You can't strip a crit or dead member of another faction!"))
- return
- attack_log += text("\[[time_stamp()]\] Has had their splints removed by [key_name(usr)]")
- usr.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(src)]'s' splints ")
- remove_splints(usr)
-
- if(href_list["tie"])
- if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr))
- if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group))
- to_chat(usr, SPAN_WARNING("You can't strip a crit or dead member of another faction!"))
- return
- if(w_uniform && istype(w_uniform, /obj/item/clothing))
- var/obj/item/clothing/under/U = w_uniform
- if(!LAZYLEN(U.accessories))
- return FALSE
- var/obj/item/clothing/accessory/A = LAZYACCESS(U.accessories, 1)
- if(LAZYLEN(U.accessories) > 1)
- A = tgui_input_list(usr, "Select an accessory to remove from [U]", "Remove accessory", U.accessories)
- if(!istype(A))
- return
- attack_log += text("\[[time_stamp()]\] Has had their accessory ([A]) removed by [key_name(usr)]")
- usr.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(src)]'s' accessory ([A])")
- if(istype(A, /obj/item/clothing/accessory/holobadge) || istype(A, /obj/item/clothing/accessory/medal))
- visible_message(SPAN_DANGER("[usr] tears off \the [A] from [src]'s [U]!"), null, null, 5)
- if(U == w_uniform)
- U.remove_accessory(usr, A)
- else
- if(HAS_TRAIT(src, TRAIT_UNSTRIPPABLE) && !is_mob_incapacitated()) //Can't strip the unstrippable!
- to_chat(usr, SPAN_DANGER("[src] has an unbreakable grip on their equipment!"))
- return
- visible_message(SPAN_DANGER("[usr] is trying to take off \a [A] from [src]'s [U]!"), null, null, 5)
- if(do_after(usr, get_strip_delay(usr, src), INTERRUPT_ALL, BUSY_ICON_GENERIC, src, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
- if(U == w_uniform)
- U.remove_accessory(usr, A)
-
if(href_list["sensor"])
if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr))
if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group))
diff --git a/code/modules/mob/living/carbon/human/human_stripping.dm b/code/modules/mob/living/carbon/human/human_stripping.dm
new file mode 100644
index 000000000000..f325db4d03ee
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/human_stripping.dm
@@ -0,0 +1,255 @@
+GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list(
+ /datum/strippable_item/mob_item_slot/head,
+ /datum/strippable_item/mob_item_slot/back,
+ /datum/strippable_item/mob_item_slot/mask,
+ /datum/strippable_item/mob_item_slot/eyes,
+ /datum/strippable_item/mob_item_slot/r_ear,
+ /datum/strippable_item/mob_item_slot/l_ear,
+ /datum/strippable_item/mob_item_slot/jumpsuit,
+ /datum/strippable_item/mob_item_slot/suit,
+ /datum/strippable_item/mob_item_slot/gloves,
+ /datum/strippable_item/mob_item_slot/feet,
+ /datum/strippable_item/mob_item_slot/suit_storage,
+ /datum/strippable_item/mob_item_slot/id,
+ /datum/strippable_item/mob_item_slot/belt,
+ /datum/strippable_item/mob_item_slot/pocket/left,
+ /datum/strippable_item/mob_item_slot/pocket/right,
+ /datum/strippable_item/mob_item_slot/hand/left,
+ /datum/strippable_item/mob_item_slot/hand/right,
+ /datum/strippable_item/mob_item_slot/cuffs/handcuffs,
+ /datum/strippable_item/mob_item_slot/cuffs/legcuffs,
+)))
+
+/mob/living/carbon/human/proc/should_strip(mob/user)
+ if (user.pulling == src && user.grab_level == GRAB_AGGRESSIVE && (user.a_intent & INTENT_GRAB))
+ return FALSE //to not interfere with fireman carry
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/head
+ key = STRIPPABLE_ITEM_HEAD
+ item_slot = SLOT_HEAD
+
+/datum/strippable_item/mob_item_slot/back
+ key = STRIPPABLE_ITEM_BACK
+ item_slot = SLOT_BACK
+
+/datum/strippable_item/mob_item_slot/mask
+ key = STRIPPABLE_ITEM_MASK
+ item_slot = SLOT_FACE
+
+/datum/strippable_item/mob_item_slot/mask/get_alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/mask = get_item(source)
+ if (!istype(mask))
+ return null
+ if (!ishuman(source))
+ return null
+ var/mob/living/carbon/human/sourcehuman = source
+ if (istype(sourcehuman.s_store, /obj/item/tank))
+ return "toggle_internals"
+ if (istype(sourcehuman.back, /obj/item/tank))
+ return "toggle_internals"
+ if (istype(sourcehuman.belt, /obj/item/tank))
+ return "toggle_internals"
+ return null
+
+/datum/strippable_item/mob_item_slot/mask/alternate_action(atom/source, mob/user)
+ if(!ishuman(source))
+ return
+ var/mob/living/carbon/human/sourcehuman = source
+ if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user))
+ return
+ if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcehuman.stat == DEAD || sourcehuman.health < HEALTH_THRESHOLD_CRIT) && !sourcehuman.get_target_lock(user.faction_group))
+ to_chat(user, SPAN_WARNING("You can't toggle internals of a crit or dead member of another faction!"))
+ return
+
+ sourcehuman.attack_log += text("\[[time_stamp()]\] Has had their internals toggled by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] Attempted to toggle [key_name(src)]'s' internals")
+ if(sourcehuman.internal)
+ user.visible_message(SPAN_DANGER("[user] is trying to disable [sourcehuman]'s internals"), null, null, 3)
+ else
+ user.visible_message(SPAN_DANGER("[user] is trying to enable [sourcehuman]'s internals."), null, null, 3)
+
+ if(do_after(user, POCKET_STRIP_DELAY, INTERRUPT_ALL, BUSY_ICON_GENERIC, sourcehuman, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
+ if(sourcehuman.internal)
+ sourcehuman.internal.add_fingerprint(user)
+ sourcehuman.internal = null
+ sourcehuman.visible_message("[sourcehuman] is no longer running on internals.", null, null, 1)
+ else
+ if(istype(sourcehuman.wear_mask, /obj/item/clothing/mask))
+ if(istype(sourcehuman.back, /obj/item/tank))
+ sourcehuman.internal = sourcehuman.back
+ else if(istype(sourcehuman.s_store, /obj/item/tank))
+ sourcehuman.internal = sourcehuman.s_store
+ else if(istype(sourcehuman.belt, /obj/item/tank))
+ sourcehuman.internal = sourcehuman.belt
+ if(sourcehuman.internal)
+ sourcehuman.visible_message(SPAN_NOTICE("[sourcehuman] is now running on internals."), null, null, 1)
+ sourcehuman.internal.add_fingerprint(user)
+
+/datum/strippable_item/mob_item_slot/eyes
+ key = STRIPPABLE_ITEM_EYES
+ item_slot = SLOT_EYES
+
+/datum/strippable_item/mob_item_slot/r_ear
+ key = STRIPPABLE_ITEM_R_EAR
+ item_slot = SLOT_EAR
+
+/datum/strippable_item/mob_item_slot/l_ear
+ key = STRIPPABLE_ITEM_L_EAR
+ item_slot = SLOT_EAR
+
+/datum/strippable_item/mob_item_slot/jumpsuit
+ key = STRIPPABLE_ITEM_JUMPSUIT
+ item_slot = SLOT_ICLOTHING
+
+/datum/strippable_item/mob_item_slot/jumpsuit/get_alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/under/uniform = get_item(source)
+ if (!istype(uniform))
+ return null
+ return uniform?.accessories ? "remove_accessory" : null
+
+/datum/strippable_item/mob_item_slot/jumpsuit/alternate_action(atom/source, mob/user)
+ if(!ishuman(source))
+ return
+ var/mob/living/carbon/human/sourcemob = source
+ if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user))
+ return
+ if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group))
+ to_chat(user, SPAN_WARNING("You can't strip a crit or dead member of another faction!"))
+ return
+ if(sourcemob.w_uniform && istype(sourcemob.w_uniform, /obj/item/clothing))
+ var/obj/item/clothing/under/uniform = sourcemob.w_uniform
+ if(!LAZYLEN(uniform.accessories))
+ return FALSE
+ var/obj/item/clothing/accessory/accessory = LAZYACCESS(uniform.accessories, 1)
+ if(LAZYLEN(uniform.accessories) > 1)
+ accessory = tgui_input_list(user, "Select an accessory to remove from [uniform]", "Remove accessory", uniform.accessories)
+ if(!istype(accessory))
+ return
+ sourcemob.attack_log += text("\[[time_stamp()]\] Has had their accessory ([accessory]) removed by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(sourcemob)]'s' accessory ([accessory])")
+ if(istype(accessory, /obj/item/clothing/accessory/holobadge) || istype(accessory, /obj/item/clothing/accessory/medal))
+ sourcemob.visible_message(SPAN_DANGER("[user] tears off \the [accessory] from [sourcemob]'s [uniform]!"), null, null, 5)
+ if(uniform == sourcemob.w_uniform)
+ uniform.remove_accessory(user, accessory)
+ else
+ if(HAS_TRAIT(sourcemob, TRAIT_UNSTRIPPABLE) && !sourcemob.is_mob_incapacitated()) //Can't strip the unstrippable!
+ to_chat(user, SPAN_DANGER("[sourcemob] has an unbreakable grip on their equipment!"))
+ return
+ sourcemob.visible_message(SPAN_DANGER("[user] is trying to take off \a [accessory] from [source]'s [uniform]!"), null, null, 5)
+ if(do_after(user, sourcemob.get_strip_delay(user, sourcemob), INTERRUPT_ALL, BUSY_ICON_GENERIC, sourcemob, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
+ if(uniform == sourcemob.w_uniform)
+ uniform.remove_accessory(user, accessory)
+
+/datum/strippable_item/mob_item_slot/suit
+ key = STRIPPABLE_ITEM_SUIT
+ item_slot = SLOT_OCLOTHING
+
+/datum/strippable_item/mob_item_slot/suit/has_no_item_alt_action()
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/suit/get_alternate_action(atom/source, mob/user)
+ if(!ishuman(source))
+ return
+ var/mob/living/carbon/human/sourcemob = source
+ for(var/bodypart in list("l_leg","r_leg","l_arm","r_arm","r_hand","l_hand","r_foot","l_foot","chest","head","groin"))
+ var/obj/limb/limb = sourcemob.get_limb(bodypart)
+ if(limb && (limb.status & LIMB_SPLINTED))
+ return "remove_splints"
+ return null
+
+/datum/strippable_item/mob_item_slot/suit/alternate_action(atom/source, mob/user)
+ if(!ishuman(source))
+ return
+ var/mob/living/carbon/human/sourcemob = source
+ if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user))
+ return
+ if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group))
+ to_chat(user, SPAN_WARNING("You can't remove splints of a crit or dead member of another faction!"))
+ return
+ sourcemob.attack_log += text("\[[time_stamp()]\] Has had their splints removed by [key_name(user)]")
+ user.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(sourcemob)]'s' splints ")
+ sourcemob.remove_splints(user)
+
+/datum/strippable_item/mob_item_slot/gloves
+ key = STRIPPABLE_ITEM_GLOVES
+ item_slot = SLOT_HANDS
+
+/datum/strippable_item/mob_item_slot/feet
+ key = STRIPPABLE_ITEM_FEET
+ item_slot = SLOT_FEET
+
+
+/datum/strippable_item/mob_item_slot/suit_storage
+ key = STRIPPABLE_ITEM_SUIT_STORAGE
+ item_slot = SLOT_SUIT_STORE
+
+/datum/strippable_item/mob_item_slot/id
+ key = STRIPPABLE_ITEM_ID
+ item_slot = SLOT_ID
+
+/datum/strippable_item/mob_item_slot/id/get_alternate_action(atom/source, mob/user)
+ var/obj/item/card/id/dogtag/tag = get_item(source)
+ if(!ishuman(source))
+ return null
+ var/mob/living/carbon/human/sourcemob = source
+ if (!istype(tag))
+ return null
+ if (!sourcemob.undefibbable && (!skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED) || sourcemob.stat != DEAD))
+ return null
+ return tag.dogtag_taken ? null : "retrieve_tag"
+
+/datum/strippable_item/mob_item_slot/id/alternate_action(atom/source, mob/user)
+ if(!ishuman(source))
+ return
+ var/mob/living/carbon/human/sourcemob = source
+ if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user))
+ return
+ if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group))
+ to_chat(user, SPAN_WARNING("You can't strip a crit or dead member of another faction!"))
+ return
+ if(!istype(sourcemob.wear_id, /obj/item/card/id/dogtag))
+ return
+ if (!sourcemob.undefibbable && !skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED))
+ return
+ var/obj/item/card/id/dogtag/tag = sourcemob.wear_id
+ if(!tag.dogtag_taken)
+ if(sourcemob.stat == DEAD)
+ to_chat(usr, SPAN_NOTICE("You take [sourcemob]'s information tag, leaving the ID tag"))
+ tag.dogtag_taken = TRUE
+ tag.icon_state = "dogtag_taken"
+ var/obj/item/dogtag/newtag = new(sourcemob.loc)
+ newtag.fallen_names = list(tag.registered_name)
+ newtag.fallen_assgns = list(tag.assignment)
+ newtag.fallen_blood_types = list(tag.blood_type)
+ user.put_in_hands(newtag)
+ else
+ to_chat(user, SPAN_WARNING("You can't take a dogtag's information tag while its owner is alive."))
+ else
+ to_chat(user, SPAN_WARNING("Someone's already taken [sourcemob]'s information tag."))
+ return
+
+
+/datum/strippable_item/mob_item_slot/belt
+ key = STRIPPABLE_ITEM_BELT
+ item_slot = SLOT_WAIST
+
+/datum/strippable_item/mob_item_slot/pocket/left
+ key = STRIPPABLE_ITEM_LPOCKET
+ item_slot = SLOT_STORE
+
+/datum/strippable_item/mob_item_slot/pocket/right
+ key = STRIPPABLE_ITEM_RPOCKET
+ item_slot = SLOT_STORE
+
+/datum/strippable_item/mob_item_slot/hand/left
+ key = STRIPPABLE_ITEM_LHAND
+
+/datum/strippable_item/mob_item_slot/hand/right
+ key = STRIPPABLE_ITEM_RHAND
+
+/datum/strippable_item/mob_item_slot/cuffs/handcuffs
+ key = STRIPPABLE_ITEM_HANDCUFFS
+
+/datum/strippable_item/mob_item_slot/cuffs/legcuffs
+ key = STRIPPABLE_ITEM_LEGCUFFS
diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm
index 3d372376d1e7..3f419333d218 100644
--- a/code/modules/mob/living/carbon/human/inventory.dm
+++ b/code/modules/mob/living/carbon/human/inventory.dm
@@ -504,70 +504,6 @@
/// Final result is overall delay * speed multiplier
return target_delay * user_speed
-/mob/living/carbon/human/stripPanelUnequip(obj/item/interact_item, mob/target_mob, slot_to_process)
- if(HAS_TRAIT(target_mob, TRAIT_UNSTRIPPABLE) && !target_mob.is_mob_incapacitated()) //Can't strip the unstrippable!
- to_chat(src, SPAN_DANGER("[target_mob] has an unbreakable grip on their equipment!"))
- return
- if(interact_item.flags_item & ITEM_ABSTRACT)
- return
- if(interact_item.flags_item & NODROP)
- to_chat(src, SPAN_WARNING("You can't remove \the [interact_item.name], it appears to be stuck!"))
- return
- if(interact_item.flags_inventory & CANTSTRIP)
- to_chat(src, SPAN_WARNING("You're having difficulty removing \the [interact_item.name]."))
- return
- target_mob.attack_log += "\[[time_stamp()]\] Has had their [interact_item.name] ([slot_to_process]) attempted to be removed by [key_name(src)]"
- attack_log += "\[[time_stamp()]\] Attempted to remove [key_name(target_mob)]'s [interact_item.name] ([slot_to_process])"
- log_interact(src, target_mob, "[key_name(src)] tried to remove [key_name(target_mob)]'s [interact_item.name] ([slot_to_process]).")
-
- src.visible_message(SPAN_DANGER("[src] tries to remove [target_mob]'s [interact_item.name]."), \
- SPAN_DANGER("You are trying to remove [target_mob]'s [interact_item.name]."), null, 5)
- interact_item.add_fingerprint(src)
- if(do_after(src, get_strip_delay(src, target_mob), INTERRUPT_ALL, BUSY_ICON_GENERIC, target_mob, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
- if(interact_item && Adjacent(target_mob) && interact_item == target_mob.get_item_by_slot(slot_to_process))
- target_mob.drop_inv_item_on_ground(interact_item)
- log_interact(src, target_mob, "[key_name(src)] removed [key_name(target_mob)]'s [interact_item.name] ([slot_to_process]) successfully.")
-
- if(target_mob)
- if(interactee == target_mob && Adjacent(target_mob))
- target_mob.show_inv(src)
-
-
-/mob/living/carbon/human/stripPanelEquip(obj/item/interact_item, mob/target_mob, slot_to_process)
- if(HAS_TRAIT(target_mob, TRAIT_UNSTRIPPABLE) && !target_mob.is_mob_incapacitated())
- to_chat(src, SPAN_DANGER("[target_mob] is too strong to force [interact_item.name] onto them!"))
- return
- if(interact_item && !(interact_item.flags_item & ITEM_ABSTRACT))
- if(interact_item.flags_item & NODROP)
- to_chat(src, SPAN_WARNING("You can't put \the [interact_item.name] on [target_mob], it's stuck to your hand!"))
- return
- if(interact_item.flags_inventory & CANTSTRIP)
- to_chat(src, SPAN_WARNING("You're having difficulty putting \the [interact_item.name] on [target_mob]."))
- return
- if(interact_item.flags_item & WIELDED)
- interact_item.unwield(src)
- if(!interact_item.mob_can_equip(target_mob, slot_to_process, TRUE))
- to_chat(src, SPAN_WARNING("You can't put \the [interact_item.name] on [target_mob]!"))
- return
- visible_message(SPAN_NOTICE("[src] tries to put \the [interact_item.name] on [target_mob]."), null, null, 5)
- log_interact(src, target_mob, "[key_name(src)] attempted to put [interact_item.name] on [key_name(target_mob)]'s ([slot_to_process]).")
- if(do_after(src, get_strip_delay(src, target_mob), INTERRUPT_ALL, BUSY_ICON_GENERIC, target_mob, INTERRUPT_MOVED, BUSY_ICON_GENERIC))
- if(interact_item == get_active_hand() && !target_mob.get_item_by_slot(slot_to_process) && Adjacent(target_mob))
- if(interact_item.flags_item & WIELDED) //to prevent re-wielding it during the do_after
- interact_item.unwield(src)
- if(interact_item.mob_can_equip(target_mob, slot_to_process, TRUE))//Placing an item on the mob
- drop_inv_item_on_ground(interact_item)
- if(interact_item && !QDELETED(interact_item)) //Might be self-deleted?
- target_mob.equip_to_slot_if_possible(interact_item, slot_to_process, 1, 0, 1, 1)
- log_interact(src, target_mob, "[key_name(src)] put [interact_item.name] on [key_name(target_mob)]'s ([slot_to_process]) successfully.")
- if(ishuman(target_mob) && target_mob.stat == DEAD)
- var/mob/living/carbon/human/human_target = target_mob
- human_target.disable_lights() // take that powergamers -spookydonut
-
- if(target_mob)
- if(interactee == target_mob && Adjacent(target_mob))
- target_mob.show_inv(src)
-
/mob/living/carbon/human/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force)
remember_dropped_object(I)
return ..()
diff --git a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
index c25b52d1dd37..b819605a6323 100644
--- a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
+++ b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
@@ -262,9 +262,6 @@
move_delay = .
-/mob/living/carbon/xenomorph/show_inv(mob/user)
- return
-
/mob/living/carbon/xenomorph/proc/pounced_mob(mob/living/L)
// This should only be called back by a mob that has pounce, so no need to check
var/datum/action/xeno_action/activable/pounce/pounceAction = get_xeno_action_by_type(src, /datum/action/xeno_action/activable/pounce)
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index dcab9f70cbb5..14f220b3a77f 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -107,23 +107,6 @@
walk(src,0)
. = ..()
-/*
- * Inventory
- */
-/mob/living/simple_animal/parrot/show_inv(mob/user as mob)
- user.set_interaction(src)
- if(user.stat) return
-
- var/dat = "Inventory of [name]
"
- if(ears)
- dat += "
Headset: [ears] (Remove)"
- else
- dat += "
Headset: Nothing"
-
- user << browse(dat, text("window=mob[];size=325x500", name))
- onclose(user, "mob[real_name]")
- return
-
/mob/living/simple_animal/parrot/Topic(href, href_list)
//Can the usr physically do this?
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index ec8aee36859f..13408be2096e 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -372,25 +372,6 @@
SIGNAL_HANDLER
reset_view(null)
-/mob/proc/show_inv(mob/user)
- user.set_interaction(src)
- var/dat = {"
-
[name]
-
-
Head(Mask): [(wear_mask ? wear_mask : "Nothing")]
-
Left Hand: [(l_hand ? l_hand : "Nothing")]
-
Right Hand: [(r_hand ? r_hand : "Nothing")]
-
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? text(" Set Internal", src) : "")]
-
[(internal ? text("Remove Internal") : "")]
-
Empty Pockets
-
Refresh
-
Close
-
"}
- show_browser(user, dat, name, "mob[name]")
- return
-
-
-
/mob/proc/point_to_atom(atom/A, turf/T)
//Squad Leaders and above have reduced cooldown and get a bigger arrow
if(check_improved_pointing())
@@ -448,21 +429,6 @@
update_flavor_text()
return
-
-/mob/MouseDrop(mob/M)
- ..()
- if(M != usr) return
- if(usr == src) return
- if(!Adjacent(usr)) return
- if(!ishuman(M) && !ismonkey(M)) return
- if(!ishuman(src) && !ismonkey(src)) return
- if(M.is_mob_incapacitated())
- return
- if(M.pulling == src && (M.a_intent & INTENT_GRAB) && M.grab_level == GRAB_AGGRESSIVE)
- return
-
- show_inv(M)
-
/mob/proc/swap_hand()
hand = !hand
SEND_SIGNAL(src, COMSIG_MOB_SWAPPED_HAND)
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 095591e313b6..84569c5884b1 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -101,6 +101,7 @@
#include "code\__DEFINES\stamina.dm"
#include "code\__DEFINES\stats.dm"
#include "code\__DEFINES\status_effects.dm"
+#include "code\__DEFINES\strippable.dm"
#include "code\__DEFINES\STUI.dm"
#include "code\__DEFINES\subsystems.dm"
#include "code\__DEFINES\supply.dm"
@@ -486,6 +487,7 @@
#include "code\datums\elements\light_blocking.dm"
#include "code\datums\elements\mouth_drop_item.dm"
#include "code\datums\elements\poor_eyesight_correction.dm"
+#include "code\datums\elements\strippable.dm"
#include "code\datums\elements\suturing.dm"
#include "code\datums\elements\yautja_tracked_item.dm"
#include "code\datums\elements\bullet_trait\damage_boost.dm"
@@ -1922,6 +1924,7 @@
#include "code\modules\mob\living\carbon\human\human_dummy.dm"
#include "code\modules\mob\living\carbon\human\human_helpers.dm"
#include "code\modules\mob\living\carbon\human\human_movement.dm"
+#include "code\modules\mob\living\carbon\human\human_stripping.dm"
#include "code\modules\mob\living\carbon\human\inventory.dm"
#include "code\modules\mob\living\carbon\human\life.dm"
#include "code\modules\mob\living\carbon\human\login.dm"
diff --git a/icons/ui_icons/inventory/back.png b/icons/ui_icons/inventory/back.png
new file mode 100644
index 000000000000..736b9d64bf99
Binary files /dev/null and b/icons/ui_icons/inventory/back.png differ
diff --git a/icons/ui_icons/inventory/belt.png b/icons/ui_icons/inventory/belt.png
new file mode 100644
index 000000000000..1be89d450a8f
Binary files /dev/null and b/icons/ui_icons/inventory/belt.png differ
diff --git a/icons/ui_icons/inventory/collar.png b/icons/ui_icons/inventory/collar.png
new file mode 100644
index 000000000000..71803b1b6c6b
Binary files /dev/null and b/icons/ui_icons/inventory/collar.png differ
diff --git a/icons/ui_icons/inventory/ears.png b/icons/ui_icons/inventory/ears.png
new file mode 100644
index 000000000000..e9a8f3c23c4b
Binary files /dev/null and b/icons/ui_icons/inventory/ears.png differ
diff --git a/icons/ui_icons/inventory/glasses.png b/icons/ui_icons/inventory/glasses.png
new file mode 100644
index 000000000000..6e6f1ad098f6
Binary files /dev/null and b/icons/ui_icons/inventory/glasses.png differ
diff --git a/icons/ui_icons/inventory/gloves.png b/icons/ui_icons/inventory/gloves.png
new file mode 100644
index 000000000000..2c8a16cbdb7a
Binary files /dev/null and b/icons/ui_icons/inventory/gloves.png differ
diff --git a/icons/ui_icons/inventory/hand_l.png b/icons/ui_icons/inventory/hand_l.png
new file mode 100644
index 000000000000..b09228d65f6d
Binary files /dev/null and b/icons/ui_icons/inventory/hand_l.png differ
diff --git a/icons/ui_icons/inventory/hand_r.png b/icons/ui_icons/inventory/hand_r.png
new file mode 100644
index 000000000000..0e05a487e070
Binary files /dev/null and b/icons/ui_icons/inventory/hand_r.png differ
diff --git a/icons/ui_icons/inventory/head.png b/icons/ui_icons/inventory/head.png
new file mode 100644
index 000000000000..11e2d2254cd0
Binary files /dev/null and b/icons/ui_icons/inventory/head.png differ
diff --git a/icons/ui_icons/inventory/id.png b/icons/ui_icons/inventory/id.png
new file mode 100644
index 000000000000..4469591d36f5
Binary files /dev/null and b/icons/ui_icons/inventory/id.png differ
diff --git a/icons/ui_icons/inventory/mask.png b/icons/ui_icons/inventory/mask.png
new file mode 100644
index 000000000000..82e510893796
Binary files /dev/null and b/icons/ui_icons/inventory/mask.png differ
diff --git a/icons/ui_icons/inventory/neck.png b/icons/ui_icons/inventory/neck.png
new file mode 100644
index 000000000000..78ad3ce3b1c7
Binary files /dev/null and b/icons/ui_icons/inventory/neck.png differ
diff --git a/icons/ui_icons/inventory/pocket.png b/icons/ui_icons/inventory/pocket.png
new file mode 100644
index 000000000000..f42399dca0f5
Binary files /dev/null and b/icons/ui_icons/inventory/pocket.png differ
diff --git a/icons/ui_icons/inventory/shoes.png b/icons/ui_icons/inventory/shoes.png
new file mode 100644
index 000000000000..d20f7ef4d106
Binary files /dev/null and b/icons/ui_icons/inventory/shoes.png differ
diff --git a/icons/ui_icons/inventory/suit.png b/icons/ui_icons/inventory/suit.png
new file mode 100644
index 000000000000..e9c48e8069f7
Binary files /dev/null and b/icons/ui_icons/inventory/suit.png differ
diff --git a/icons/ui_icons/inventory/suit_storage.png b/icons/ui_icons/inventory/suit_storage.png
new file mode 100644
index 000000000000..9722eb10297a
Binary files /dev/null and b/icons/ui_icons/inventory/suit_storage.png differ
diff --git a/icons/ui_icons/inventory/uniform.png b/icons/ui_icons/inventory/uniform.png
new file mode 100644
index 000000000000..292b3324b5bd
Binary files /dev/null and b/icons/ui_icons/inventory/uniform.png differ
diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx
new file mode 100644
index 000000000000..2ab729b2f1ce
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/StripMenu.tsx
@@ -0,0 +1,405 @@
+import { range } from 'common/collections';
+import { BooleanLike } from 'common/react';
+import { resolveAsset } from '../assets';
+import { useBackend } from '../backend';
+import { Box, Button, Icon, Stack } from '../components';
+import { Window } from '../layouts';
+
+const ROWS = 5;
+const COLUMNS = 6;
+
+const BUTTON_DIMENSIONS = '50px';
+
+type GridSpotKey = string;
+
+const getGridSpotKey = (spot: [number, number]): GridSpotKey => {
+ return `${spot[0]}/${spot[1]}`;
+};
+
+const CornerText = (props: {
+ readonly align: 'left' | 'right';
+ readonly children: string;
+}): JSX.Element => {
+ const { align, children } = props;
+
+ return (
+
+ {children}
+
+ );
+};
+
+type AlternateAction = {
+ icon: string;
+ text: string;
+};
+
+const ALTERNATE_ACTIONS: Record = {
+ remove_splints: {
+ icon: 'crutch',
+ text: 'Remove splints',
+ },
+
+ remove_accessory: {
+ icon: 'tshirt',
+ text: 'Remove accessory',
+ },
+
+ retrieve_tag: {
+ icon: 'tags',
+ text: 'Retrieve info tag',
+ },
+
+ toggle_internals: {
+ icon: 'mask-face',
+ text: 'Toggle internals',
+ },
+};
+
+type Slot = {
+ displayName: string;
+ gridSpot: GridSpotKey;
+ image?: string;
+ additionalComponent?: JSX.Element;
+ hideEmpty?: boolean;
+};
+
+const SLOTS: Record = {
+ glasses: {
+ displayName: 'glasses',
+ gridSpot: getGridSpotKey([0, 1]),
+ image: 'inventory-glasses.png',
+ },
+
+ head: {
+ displayName: 'headwear',
+ gridSpot: getGridSpotKey([0, 2]),
+ image: 'inventory-head.png',
+ },
+
+ wear_mask: {
+ displayName: 'mask',
+ gridSpot: getGridSpotKey([1, 2]),
+ image: 'inventory-mask.png',
+ },
+
+ wear_r_ear: {
+ displayName: 'right earwear',
+ gridSpot: getGridSpotKey([0, 3]),
+ image: 'inventory-ears.png',
+ },
+
+ wear_l_ear: {
+ displayName: 'left earwear',
+ gridSpot: getGridSpotKey([1, 3]),
+ image: 'inventory-ears.png',
+ },
+
+ handcuffs: {
+ displayName: 'handcuffs',
+ gridSpot: getGridSpotKey([1, 4]),
+ hideEmpty: true,
+ },
+
+ legcuffs: {
+ displayName: 'legcuffs',
+ gridSpot: getGridSpotKey([1, 5]),
+ hideEmpty: true,
+ },
+
+ w_uniform: {
+ displayName: 'uniform',
+ gridSpot: getGridSpotKey([2, 1]),
+ image: 'inventory-uniform.png',
+ },
+
+ wear_suit: {
+ displayName: 'suit',
+ gridSpot: getGridSpotKey([2, 2]),
+ image: 'inventory-suit.png',
+ },
+
+ gloves: {
+ displayName: 'gloves',
+ gridSpot: getGridSpotKey([2, 3]),
+ image: 'inventory-gloves.png',
+ },
+
+ r_hand: {
+ displayName: 'right hand',
+ gridSpot: getGridSpotKey([2, 4]),
+ image: 'inventory-hand_r.png',
+ additionalComponent: R,
+ },
+
+ l_hand: {
+ displayName: 'left hand',
+ gridSpot: getGridSpotKey([2, 5]),
+ image: 'inventory-hand_l.png',
+ additionalComponent: L,
+ },
+
+ shoes: {
+ displayName: 'shoes',
+ gridSpot: getGridSpotKey([3, 2]),
+ image: 'inventory-shoes.png',
+ },
+
+ j_store: {
+ displayName: 'suit storage item',
+ gridSpot: getGridSpotKey([4, 0]),
+ image: 'inventory-suit_storage.png',
+ },
+
+ id: {
+ displayName: 'ID',
+ gridSpot: getGridSpotKey([4, 1]),
+ image: 'inventory-id.png',
+ },
+
+ belt: {
+ displayName: 'belt',
+ gridSpot: getGridSpotKey([4, 2]),
+ image: 'inventory-belt.png',
+ },
+
+ back: {
+ displayName: 'backpack',
+ gridSpot: getGridSpotKey([4, 3]),
+ image: 'inventory-back.png',
+ },
+
+ l_store: {
+ displayName: 'left pocket',
+ gridSpot: getGridSpotKey([4, 4]),
+ image: 'inventory-pocket.png',
+ },
+
+ r_store: {
+ displayName: 'right pocket',
+ gridSpot: getGridSpotKey([4, 5]),
+ image: 'inventory-pocket.png',
+ },
+};
+
+enum ObscuringLevel {
+ Completely = 1,
+ Hidden = 2,
+}
+
+type Interactable = {
+ interacting: BooleanLike;
+};
+
+/**
+ * Some possible options:
+ *
+ * null - No interactions, no item, but is an available slot
+ * { interacting: 1 } - No item, but we're interacting with it
+ * { icon: icon, name: name } - An item with no alternate actions
+ * that we're not interacting with.
+ * { icon, name, interacting: 1 } - An item with no alternate actions
+ * that we're interacting with.
+ */
+type StripMenuItem =
+ | null
+ | Interactable
+ | ((
+ | {
+ icon: string;
+ name: string;
+ alternate: string;
+ }
+ | {
+ obscured: ObscuringLevel;
+ }
+ | {
+ no_item_action: string;
+ }
+ ) &
+ Partial);
+
+type StripMenuData = {
+ items: Record;
+ name: string;
+};
+
+export const StripMenu = (props, context) => {
+ const { act, data } = useBackend();
+
+ const gridSpots = new Map();
+ for (const key of Object.keys(data.items)) {
+ const item = data.items[key];
+ if (item === null && SLOTS[key].hideEmpty) continue;
+ gridSpots.set(SLOTS[key].gridSpot, key);
+ }
+
+ return (
+
+
+
+ {range(0, ROWS).map((row) => (
+
+
+ {range(0, COLUMNS).map((column) => {
+ const key = getGridSpotKey([row, column]);
+ const keyAtSpot = gridSpots.get(key);
+
+ if (!keyAtSpot) {
+ return (
+
+ );
+ }
+
+ const item = data.items[keyAtSpot];
+ const slot = SLOTS[keyAtSpot];
+
+ let alternateAction: AlternateAction | undefined;
+
+ let content;
+ let tooltip;
+
+ if (item === null) {
+ tooltip = slot.displayName;
+ } else if ('name' in item) {
+ alternateAction = ALTERNATE_ACTIONS[item.alternate];
+
+ content = (
+
+ );
+
+ tooltip = item.name;
+ } else if ('obscured' in item) {
+ content = (
+
+ );
+
+ tooltip = `obscured ${slot.displayName}`;
+ } else if ('no_item_action' in item) {
+ tooltip = slot.displayName;
+ alternateAction = ALTERNATE_ACTIONS[item.no_item_action];
+ }
+
+ return (
+
+
+
+
+ {alternateAction !== undefined && (
+
+ )}
+
+
+ );
+ })}
+
+
+ ))}
+
+
+
+ );
+};