diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 46ca84f33dd..01ff3f8d43c 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -316,7 +316,7 @@
#define APPRENTICE_AGE_MIN 29 //youngest an apprentice can be
#define SHOES_SLOWDOWN 0 //How much shoes slow you down by default. Negative values speed you up
#define SHOES_SPEED_SLIGHT SHOES_SLOWDOWN - 1 // slightest speed boost to movement
-#define POCKET_STRIP_DELAY 40 //time taken (in deciseconds) to search somebody's pockets
+#define POCKET_STRIP_DELAY (4 SECONDS) //time taken to search somebody's pockets
#define DOOR_CRUSH_DAMAGE 15 //the amount of damage that airlocks deal when they crush you
#define HUNGER_FACTOR 0.05 //factor at which mob nutrition decreases
diff --git a/code/__DEFINES/strippable.dm b/code/__DEFINES/strippable.dm
new file mode 100644
index 00000000000..a660e827842
--- /dev/null
+++ b/code/__DEFINES/strippable.dm
@@ -0,0 +1,31 @@
+// All of these must be matched in StripMenu.js.
+#define STRIPPABLE_ITEM_HEAD "head"
+#define STRIPPABLE_ITEM_BACK "back"
+#define STRIPPABLE_ITEM_MASK "mask"
+#define STRIPPABLE_ITEM_NECK "neck"
+#define STRIPPABLE_ITEM_EYES "eyes"
+#define STRIPPABLE_ITEM_EARS "ears"
+#define STRIPPABLE_ITEM_JUMPSUIT "jumpsuit"
+#define STRIPPABLE_ITEM_SUIT "suit"
+#define STRIPPABLE_ITEM_GLOVES "gloves"
+#define STRIPPABLE_ITEM_FEET "shoes"
+#define STRIPPABLE_ITEM_SUIT_STORAGE "suit_storage"
+#define STRIPPABLE_ITEM_ID "id"
+#define STRIPPABLE_ITEM_BELT "belt"
+#define STRIPPABLE_ITEM_LPOCKET "left_pocket"
+#define STRIPPABLE_ITEM_RPOCKET "right_pocket"
+#define STRIPPABLE_ITEM_LHAND "left_hand"
+#define STRIPPABLE_ITEM_RHAND "right_hand"
+#define STRIPPABLE_ITEM_HANDCUFFS "handcuffs"
+#define STRIPPABLE_ITEM_LEGCUFFS "legcuffs"
+#define STRIPPABLE_ITEM_CORGI_COLLAR "corgi_collar"
+#define STRIPPABLE_ITEM_PARROT_HEADSET "parrot_headset"
+
+/// 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 7759fe4a394..a3a874f9f07 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -9,7 +9,7 @@
* Misc
*/
-#define LAZYINITLIST(L) if (!L) L = list()
+#define LAZYINITLIST(L) if (!L) { L = list(); }
#define UNSETEMPTY(L) if (L && !length(L)) L = null
///Like LAZYCOPY - copies an input list if the list has entries, If it doesn't the assigned list is nulled
#define LAZYLISTDUPLICATE(L) (L ? L.Copy() : null )
@@ -34,6 +34,12 @@
#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
#define reverseList(L) reverseRange(L.Copy())
+/// 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;
+
/// Passed into BINARY_INSERT to compare keys
#define COMPARE_KEY __BIN_LIST[__BIN_MID]
/// Passed into BINARY_INSERT to compare values
diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm
new file mode 100644
index 00000000000..b2d53de019b
--- /dev/null
+++ b/code/datums/elements/strippable.dm
@@ -0,0 +1,488 @@
+/// 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_MOUSEDROP_ONTO, .proc/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_MOUSEDROP_ONTO)
+
+ 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 != user)
+ return
+
+ // Cyborgs buckle people by dragging them onto them, unless in combat mode.
+ if (iscyborg(user))
+ var/mob/living/silicon/robot/cyborg_user = user
+ if (!cyborg_user.combat_mode)
+ return
+
+ if (!isnull(should_strip_proc_path) && !call(source, should_strip_proc_path)(user))
+ 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, /datum/.proc/ui_interact, user)
+
+/// 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 (HAS_TRAIT(equipping, TRAIT_NODROP))
+ to_chat(user, "You can't put [equipping] on [source], it's stuck to your hand!")
+ return FALSE
+ 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)
+ if (warn_dangerous_clothing && isclothing(source))
+ var/obj/item/clothing/clothing = source
+ if(clothing.clothing_flags & DANGEROUS_OBJECT)
+ source.visible_message(
+ "[user] tries to put [equipping] on [source].",
+ "[user] tries to put [equipping] on you.",
+ ignored_mobs = user,
+ )
+ else
+ source.visible_message(
+ "[user] tries to put [equipping] on [source].",
+ "[user] tries to put [equipping] on you.",
+ ignored_mobs = user,
+ )
+
+ to_chat(user, "You try to put [equipping] on [source]...")
+
+ var/log = "[key_name(source)] is having [equipping] put on them by [key_name(user)]"
+ source.log_message(log, LOG_ATTACK, color="red")
+ user.log_message(log, LOG_ATTACK, color="red", log_globally=FALSE)
+
+ 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 (ismob(source))
+ var/mob/mob_source = source
+ if (!item.canStrip(user, mob_source))
+ return FALSE
+
+ 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(
+ "[user] tries to remove [source]'s [item].",
+ "[user] tries to remove your [item].",
+ ignored_mobs = user,
+ )
+
+ to_chat(user, "You try to remove [source]'s [item]...")
+ source.log_message("[key_name(source)] is being stripped of [item] by [key_name(src)]", LOG_ATTACK, color="red")
+ user.log_message("[key_name(source)] is being stripped of [item] by [key_name(src)]", LOG_ATTACK, color="red", log_globally=FALSE)
+ item.add_fingerprint(src)
+
+ 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/mob_item_slot/get_item(atom/source)
+ if (!ismob(source))
+ return null
+
+ var/mob/mob_source = source
+ return mob_source.get_item_by_slot(item_slot)
+
+/datum/strippable_item/mob_item_slot/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ if (!ismob(source))
+ return FALSE
+
+ if (!equipping.mob_can_equip(
+ source,
+ user,
+ item_slot,
+ disable_warning = TRUE,
+ bypass_equip_delay_self = TRUE,
+ ))
+ to_chat(user, "\The [equipping] doesn't fit in that place!")
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/start_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ if (!ismob(source))
+ return FALSE
+
+ if (!do_mob(user, source, get_equip_delay(equipping)))
+ return FALSE
+
+ if (!equipping.mob_can_equip(
+ source,
+ user,
+ item_slot,
+ disable_warning = TRUE,
+ bypass_equip_delay_self = TRUE,
+ ))
+ return FALSE
+
+ if (!user.temporarilyRemoveItemFromInventory(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/mob_source = source
+ mob_source.equip_to_slot(equipping, item_slot)
+
+/datum/strippable_item/mob_item_slot/get_obscuring(atom/source)
+ if (iscarbon(source))
+ var/mob/living/carbon/carbon_source = source
+ return (carbon_source.check_obscured_slots() & item_slot) \
+ ? STRIPPABLE_OBSCURING_COMPLETELY \
+ : STRIPPABLE_OBSCURING_NONE
+
+ 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)
+
+/// Returns the delay of equipping this item to a mob
+/datum/strippable_item/mob_item_slot/proc/get_equip_delay(obj/item/equipping)
+ return equipping.equip_delay_other
+
+/// A utility function for `/datum/strippable_item`s to start unequipping an item from a mob.
+/proc/start_unequip_mob(obj/item/item, mob/source, mob/user, strip_delay)
+ if (!do_mob(user, source, strip_delay || item.strip_delay, interaction_key = REF(item)))
+ return FALSE
+
+ return TRUE
+
+/// A utility function for `/datum/strippable_item`s to finish unequipping an item from a mob.
+/proc/finish_unequip_mob(obj/item/item, mob/source, mob/user)
+ if (!item.doStrip(user, source))
+ return FALSE
+
+ source.log_message("[key_name(source)] has been stripped of [item] by [key_name(user)]", LOG_ATTACK, color="red")
+ user.log_message("[key_name(source)] has been stripped of [item] by [key_name(user)]", LOG_ATTACK, color="red", log_globally=FALSE)
+
+ // Updates speed in case stripped speed affecting item
+ source.update_equipment_speed_mods()
+
+/// 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/ui_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))
+ items[strippable_key] = result
+ continue
+
+ LAZYINITLIST(result)
+
+ result["icon"] = icon2base64(icon(item.icon, item.icon_state))
+ 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 = usr
+
+ switch (action)
+ if ("use")
+ 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))
+ var/obj/item/held_item = user.get_active_held_item()
+ if (isnull(held_item))
+ return
+
+ if (strippable_item.try_equip(owner, held_item, user))
+ 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)
+ else if (strippable_item.try_unequip(owner, user))
+ 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))
+ 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.dm b/code/game/objects/items.dm
index 8cecf0c3440..f2ac4d8a8c8 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -981,7 +981,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/proc/canStrip(mob/stripper, mob/owner)
SHOULD_BE_PURE(TRUE)
- return !HAS_TRAIT(src, TRAIT_NODROP)
+ return !HAS_TRAIT(src, TRAIT_NODROP) && !(item_flags & ABSTRACT)
/obj/item/proc/doStrip(mob/stripper, mob/owner)
return owner.dropItemToGround(src)
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 60f8bb05f44..2111d20f6eb 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -527,6 +527,27 @@
Insert(id, fish_icon, fish_icon_state)
..()
+/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',
+ )
+
/// Removes all non-alphanumerics from the text, keep in mind this can lead to id conflicts
/proc/sanitize_css_class_name(name)
var/static/regex/regex = new(@"[^a-zA-Z0-9]","g")
diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm
index b4aa384af9d..95b011deb04 100644
--- a/code/modules/clothing/neck/_neck.dm
+++ b/code/modules/clothing/neck/_neck.dm
@@ -173,7 +173,7 @@
icon_state = "petcollar"
var/tagname = null
-/obj/item/clothing/neck/petcollar/mob_can_equip(mob/M, mob/equipper, slot, disable_warning = 0)
+/obj/item/clothing/neck/petcollar/mob_can_equip(mob/M, mob/living/equipper, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE)
if(!ismonkey(M))
return FALSE
return ..()
diff --git a/code/modules/clothing/shoes/_shoes.dm b/code/modules/clothing/shoes/_shoes.dm
index 5ed50ad872f..0fd4b67581c 100644
--- a/code/modules/clothing/shoes/_shoes.dm
+++ b/code/modules/clothing/shoes/_shoes.dm
@@ -136,6 +136,13 @@
if(!istype(our_guy))
return
+ if (!isliving(user))
+ return
+
+ var/mob/living/living_user = user
+ if (!(living_user.mobility_flags & MOBILITY_USE))
+ return
+
if(!in_range(user, our_guy))
to_chat(user, "You aren't close enough to interact with [src]'s laces!")
return
diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm
index c3db4e56d00..319d69d2238 100644
--- a/code/modules/clothing/spacesuits/hardsuit.dm
+++ b/code/modules/clothing/spacesuits/hardsuit.dm
@@ -702,7 +702,7 @@
armor = list(MELEE = 30, BULLET = 5, LASER = 10, ENERGY = 20, BOMB = 10, BIO = 100, RAD = 75, FIRE = 60, ACID = 30)
helmettype = /obj/item/clothing/head/helmet/space/hardsuit/clown
-/obj/item/clothing/suit/space/hardsuit/clown/mob_can_equip(mob/M, slot)
+/obj/item/clothing/suit/space/hardsuit/clown/mob_can_equip(mob/M, mob/living/equipper, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE)
if(!..() || !ishuman(M))
return FALSE
var/mob/living/carbon/human/H = M
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
index b3c8ed8a46b..63a4f53eeeb 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
@@ -7,8 +7,6 @@
hud_type = /datum/hud/alien
melee_damage_lower = 20 //Refers to unarmed damage, aliens do unarmed attacks.
melee_damage_upper = 20
- var/obj/item/r_store = null
- var/obj/item/l_store = null
var/caste = ""
var/alt_icon = 'icons/mob/alienleap.dmi' //used to switch between the two alien icon files.
var/leap_on_click = 0
@@ -26,46 +24,17 @@
/obj/item/bodypart/l_leg/alien,
)
+GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list(
+ /datum/strippable_item/hand/left,
+ /datum/strippable_item/hand/right,
+ /datum/strippable_item/mob_item_slot/handcuffs,
+ /datum/strippable_item/mob_item_slot/legcuffs,
+)))
+
/mob/living/carbon/alien/humanoid/Initialize()
. = ..()
AddComponent(/datum/component/footstep, FOOTSTEP_MOB_CLAW, 0.5, -11)
-
-
-/mob/living/carbon/alien/humanoid/show_inv(mob/user)
- user.set_machine(src)
- var/list/dat = list()
- dat += "
- Close
- "}
-
- var/datum/browser/popup = new(user, "mob[REF(src)]", "[src]", 440, 510)
- popup.set_content(dat.Join())
- popup.open()
-
-
-/mob/living/carbon/alien/humanoid/Topic(href, href_list)
- //strip panel
- if(href_list["pouches"] && usr.canUseTopic(src, BE_CLOSE, NO_DEXTERITY))
- visible_message("[usr] tries to empty [src]'s pouches.", \
- "[usr] tries to empty your pouches.")
- if(do_mob(usr, src, POCKET_STRIP_DELAY * 0.5))
- dropItemToGround(r_store)
- dropItemToGround(l_store)
-
- ..()
-
+ AddElement(/datum/element/strippable, GLOB.strippable_alien_humanoid_items)
/mob/living/carbon/alien/humanoid/cuff_resist(obj/item/I)
playsound(src, 'sound/voice/hiss5.ogg', 40, TRUE, TRUE) //Alien roars when starting to break free
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index f5fdbf63203..23baa12e7a2 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -64,10 +64,6 @@
// new damage icon system
// now constructs damage icon for each organ from mask * damage field
-
-/mob/living/carbon/alien/larva/show_inv(mob/user)
- return
-
/mob/living/carbon/alien/larva/toggle_throw_mode()
return
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 81a3801ed12..7e6e7005178 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -192,70 +192,8 @@
/mob/living/carbon/proc/canBeHandcuffed()
return FALSE
-
-/mob/living/carbon/show_inv(mob/user)
- user.set_machine(src)
- var/has_breathable_mask = istype(wear_mask, /obj/item/clothing/mask)
- var/obscured = check_obscured_slots()
- var/list/dat = list()
-
- dat += "
- Close
- "}
-
- var/datum/browser/popup = new(user, "mob[REF(src)]", "[src]", 440, 510)
- popup.set_content(dat.Join())
- popup.open()
-
/mob/living/carbon/Topic(href, href_list)
..()
- //strip panel
- if(href_list["internal"] && usr.canUseTopic(src, BE_CLOSE, NO_DEXTERITY))
- var/slot = text2num(href_list["internal"])
- var/obj/item/ITEM = get_item_by_slot(slot)
- if(ITEM && istype(ITEM, /obj/item/tank) && wear_mask && (wear_mask.clothing_flags & MASKINTERNALS))
- visible_message("[usr] tries to [internal ? "close" : "open"] the valve on [src]'s [ITEM.name].", \
- "[usr] tries to [internal ? "close" : "open"] the valve on your [ITEM.name].", null, null, usr)
- to_chat(usr, "You try to [internal ? "close" : "open"] the valve on [src]'s [ITEM.name]...")
- if(do_mob(usr, src, POCKET_STRIP_DELAY))
- if(internal)
- internal = null
- update_internals_hud_icon(0)
- else if(ITEM && istype(ITEM, /obj/item/tank))
- if((wear_mask && (wear_mask.clothing_flags & MASKINTERNALS)) || getorganslot(ORGAN_SLOT_BREATHING_TUBE))
- internal = ITEM
- update_internals_hud_icon(1)
-
- visible_message("[usr] [internal ? "opens" : "closes"] the valve on [src]'s [ITEM.name].", \
- "[usr] [internal ? "opens" : "closes"] the valve on your [ITEM.name].", null, null, usr)
- to_chat(usr, "You [internal ? "open" : "close"] the valve on [src]'s [ITEM.name].")
-
if(href_list["embedded_object"] && usr.canUseTopic(src, BE_CLOSE, NO_DEXTERITY))
var/obj/item/bodypart/L = locate(href_list["embedded_limb"]) in bodyparts
if(!L)
diff --git a/code/modules/mob/living/carbon/carbon_stripping.dm b/code/modules/mob/living/carbon/carbon_stripping.dm
new file mode 100644
index 00000000000..c5969d57c60
--- /dev/null
+++ b/code/modules/mob/living/carbon/carbon_stripping.dm
@@ -0,0 +1,129 @@
+/datum/strippable_item/mob_item_slot/head
+ key = STRIPPABLE_ITEM_HEAD
+ item_slot = ITEM_SLOT_HEAD
+
+/datum/strippable_item/mob_item_slot/back
+ key = STRIPPABLE_ITEM_BACK
+ item_slot = ITEM_SLOT_BACK
+
+/datum/strippable_item/mob_item_slot/mask
+ key = STRIPPABLE_ITEM_MASK
+ item_slot = ITEM_SLOT_MASK
+
+/datum/strippable_item/mob_item_slot/neck
+ key = STRIPPABLE_ITEM_NECK
+ item_slot = ITEM_SLOT_NECK
+
+/datum/strippable_item/mob_item_slot/handcuffs
+ key = STRIPPABLE_ITEM_HANDCUFFS
+ item_slot = ITEM_SLOT_HANDCUFFED
+
+/datum/strippable_item/mob_item_slot/handcuffs/should_show(atom/source, mob/user)
+ if (!iscarbon(source))
+ return FALSE
+
+ var/mob/living/carbon/carbon_source = source
+ return !isnull(carbon_source.handcuffed)
+
+// You shouldn't be able to equip things to handcuff slots.
+/datum/strippable_item/mob_item_slot/handcuffs/try_equip(atom/source, obj/item/equipping, mob/user)
+ return FALSE
+
+/datum/strippable_item/mob_item_slot/legcuffs
+ key = STRIPPABLE_ITEM_LEGCUFFS
+ item_slot = ITEM_SLOT_LEGCUFFED
+
+/datum/strippable_item/mob_item_slot/legcuffs/should_show(atom/source, mob/user)
+ if (!iscarbon(source))
+ return FALSE
+
+ var/mob/living/carbon/carbon_source = source
+ return !isnull(carbon_source.legcuffed)
+
+// You shouldn't be able to equip things to legcuff slots.
+/datum/strippable_item/mob_item_slot/legcuffs/try_equip(atom/source, obj/item/equipping, mob/user)
+ return FALSE
+
+/// A strippable item for a hand
+/datum/strippable_item/hand
+ // Putting dangerous clothing in our hand is fine.
+ warn_dangerous_clothing = FALSE
+
+ /// Which hand?
+ var/hand_index
+
+/datum/strippable_item/hand/get_item(atom/source)
+ if (!ismob(source))
+ return null
+
+ var/mob/mob_source = source
+ return mob_source.get_item_for_held_index(hand_index)
+
+/datum/strippable_item/hand/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ if (!ismob(source))
+ return FALSE
+
+ var/mob/mob_source = source
+
+ if (!mob_source.can_put_in_hand(equipping, hand_index))
+ to_chat(src, "\The [equipping] doesn't fit in that place!")
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/hand/start_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ if (!ismob(source))
+ return FALSE
+
+ var/mob/mob_source = source
+
+ if (!do_mob(user, source, equipping.equip_delay_other))
+ return FALSE
+
+ if (!mob_source.can_put_in_hand(equipping, hand_index))
+ return FALSE
+
+ if (!user.temporarilyRemoveItemFromInventory(equipping))
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/hand/finish_equip(atom/source, obj/item/equipping, mob/user)
+ if (!iscarbon(source))
+ return FALSE
+
+ var/mob/mob_source = source
+ mob_source.put_in_hand(equipping, hand_index)
+
+/datum/strippable_item/hand/start_unequip(atom/source, mob/user)
+ . = ..()
+ if (!.)
+ return
+
+ return start_unequip_mob(get_item(source), source, user)
+
+/datum/strippable_item/hand/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)
+
+/datum/strippable_item/hand/left
+ key = STRIPPABLE_ITEM_LHAND
+ hand_index = 1
+
+/datum/strippable_item/hand/right
+ key = STRIPPABLE_ITEM_RHAND
+ hand_index = 2
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 686194c3d0b..17685a54fb3 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -23,6 +23,7 @@
AddComponent(/datum/component/footstep, FOOTSTEP_MOB_HUMAN, 1, -6)
AddComponent(/datum/component/bloodysoles/feet)
AddElement(/datum/element/ridable, /datum/component/riding/creature/human)
+ AddElement(/datum/element/strippable, GLOB.strippable_human_items, /mob/living/carbon/human/.proc/should_strip)
GLOB.human_list += src
/mob/living/carbon/human/proc/setup_human_dna()
@@ -83,110 +84,6 @@
. += "Chemical Storage: [changeling.chem_charges]/[changeling.chem_storage]"
. += "Absorbed DNA: [changeling.absorbedcount]"
-/mob/living/carbon/human/show_inv(mob/user)
- user.set_machine(src)
- var/has_breathable_mask = istype(wear_mask, /obj/item/clothing/mask)
- var/obscured = check_obscured_slots()
- var/list/dat = list()
-
- dat += "
- Close
- "}
-
- var/datum/browser/popup = new(user, "mob[REF(src)]", "[src]", 440, 510)
- popup.set_content(dat.Join())
- popup.open()
-
// called when something steps onto a human
/mob/living/carbon/human/Crossed(atom/movable/AM)
. = ..()
@@ -209,59 +106,6 @@
to_chat(usr, "You can't reach that! Something is covering it.")
return
- if(href_list["pockets"] && usr.canUseTopic(src, BE_CLOSE, NO_DEXTERITY)) //TODO: Make it match (or intergrate it into) strippanel so you get 'item cannot fit here' warnings if mob_can_equip fails
- var/pocket_side = href_list["pockets"] != "right" ? "left" : "right"
- var/pocket_id = (pocket_side == "right" ? ITEM_SLOT_RPOCKET : ITEM_SLOT_LPOCKET)
- var/obj/item/pocket_item = (pocket_id == ITEM_SLOT_RPOCKET ? r_store : l_store)
- var/obj/item/place_item = usr.get_active_held_item() // Item to place in the pocket, if it's empty
-
- var/delay_denominator = 1
- if(pocket_item && !(pocket_item.item_flags & ABSTRACT))
- if(HAS_TRAIT(pocket_item, TRAIT_NODROP))
- to_chat(usr, "You try to empty [src]'s [pocket_side] pocket, it seems to be stuck!")
- to_chat(usr, "You try to empty [src]'s [pocket_side] pocket.")
- log_message("[key_name(src)] is having their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket emptied of [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red")
- usr.log_message("[key_name(src)] is having their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket emptied of [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red", log_globally=FALSE)
- else if(place_item && place_item.mob_can_equip(src, usr, pocket_id, 1) && !(place_item.item_flags & ABSTRACT))
- to_chat(usr, "You try to place [place_item] into [src]'s [pocket_side] pocket.")
- log_message("[key_name(src)] is having their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket filled with [place_item] by [key_name(usr)]", LOG_ATTACK, color="red")
- usr.log_message("[key_name(src)] is having their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket filled with [place_item] by [key_name(usr)]", LOG_ATTACK, color="red", log_globally=FALSE)
- delay_denominator = 4
- else
- return
-
- if(do_mob(usr, src, POCKET_STRIP_DELAY/delay_denominator)) //placing an item into the pocket is 4 times faster
- if(pocket_item)
- if(pocket_item == (pocket_id == ITEM_SLOT_RPOCKET ? r_store : l_store)) //item still in the pocket we search
- dropItemToGround(pocket_item)
- log_message("[key_name(src)] had their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket emptied of [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red")
- usr.log_message("[key_name(src)] had their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket emptied of [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red", log_globally=FALSE)
- else
- if(place_item)
- if(place_item.mob_can_equip(src, usr, pocket_id, FALSE, TRUE))
- usr.temporarilyRemoveItemFromInventory(place_item, TRUE)
- equip_to_slot(place_item, pocket_id, TRUE)
- log_message("[key_name(src)] had their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket filled with [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red")
- usr.log_message("[key_name(src)] had their [pocket_id == ITEM_SLOT_RPOCKET ? "right" : "left"] pocket filled with [pocket_item] by [key_name(usr)]", LOG_ATTACK, color="red", log_globally=FALSE)
- //do nothing otherwise
- //updating inv screen after handled by living/Topic()
- else
- // Display a warning if the user mocks up
- to_chat(src, "You feel your [pocket_side] pocket being fumbled with!")
-
- if(href_list["toggle_uniform"] && usr.canUseTopic(src, BE_CLOSE, NO_DEXTERITY))
- var/obj/item/clothing/under/U = get_item_by_slot(ITEM_SLOT_ICLOTHING)
- to_chat(src, "[usr.name] is trying to adjust your [U].")
- if(do_mob(usr, src, U.strip_delay/2))
- to_chat(src, "[usr.name] successfully adjusted your [U].")
- U.toggle_jumpsuit_adjust()
- update_inv_w_uniform()
- update_body()
-
- var/mob/living/user = usr
- if(istype(user) && href_list["shoes"] && shoes && (user.mobility_flags & MOBILITY_USE)) // we need to be on the ground, so we'll be a bit looser
- shoes.handle_tying(usr)
-
///////HUDs///////
if(href_list["hud"])
if(!ishuman(usr))
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 00000000000..98cfc6824aa
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/human_stripping.dm
@@ -0,0 +1,232 @@
+#define INTERNALS_TOGGLE_DELAY (4 SECONDS)
+#define POCKET_EQUIP_DELAY (1 SECONDS)
+
+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/neck,
+ /datum/strippable_item/mob_item_slot/eyes,
+ /datum/strippable_item/mob_item_slot/ears,
+ /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/hand/left,
+ /datum/strippable_item/hand/right,
+ /datum/strippable_item/mob_item_slot/handcuffs,
+ /datum/strippable_item/mob_item_slot/legcuffs,
+)))
+
+/mob/living/carbon/human/proc/should_strip(mob/user)
+ if (user.pulling != src || user.grab_state != GRAB_AGGRESSIVE)
+ return TRUE
+
+ if (ishuman(user))
+ var/mob/living/carbon/human/human_user = user
+ return !human_user.can_be_firemanned(src)
+
+ return TRUE
+
+/datum/strippable_item/mob_item_slot/eyes
+ key = STRIPPABLE_ITEM_EYES
+ item_slot = ITEM_SLOT_EYES
+
+/datum/strippable_item/mob_item_slot/ears
+ key = STRIPPABLE_ITEM_EARS
+ item_slot = ITEM_SLOT_EARS
+
+/datum/strippable_item/mob_item_slot/jumpsuit
+ key = STRIPPABLE_ITEM_JUMPSUIT
+ item_slot = ITEM_SLOT_ICLOTHING
+
+/datum/strippable_item/mob_item_slot/jumpsuit/get_alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/under/jumpsuit = get_item(source)
+ if (!istype(jumpsuit))
+ return null
+ return jumpsuit?.can_adjust ? "adjust_jumpsuit" : null
+
+/datum/strippable_item/mob_item_slot/jumpsuit/alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/under/jumpsuit = get_item(source)
+ if (!istype(jumpsuit))
+ return null
+ to_chat(source, "[user] is trying to adjust your [jumpsuit.name].")
+ if (!do_mob(user, source, jumpsuit.strip_delay * 0.5))
+ return
+ to_chat(source, "[user] successfully adjusted your [jumpsuit.name].")
+ jumpsuit.toggle_jumpsuit_adjust()
+
+ if (!ismob(source))
+ return
+
+ var/mob/mob_source = source
+ mob_source.update_inv_w_uniform()
+ mob_source.update_body()
+
+/datum/strippable_item/mob_item_slot/suit
+ key = STRIPPABLE_ITEM_SUIT
+ item_slot = ITEM_SLOT_OCLOTHING
+
+/datum/strippable_item/mob_item_slot/gloves
+ key = STRIPPABLE_ITEM_GLOVES
+ item_slot = ITEM_SLOT_GLOVES
+
+/datum/strippable_item/mob_item_slot/feet
+ key = STRIPPABLE_ITEM_FEET
+ item_slot = ITEM_SLOT_FEET
+
+/datum/strippable_item/mob_item_slot/feet/get_alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/shoes/shoes = get_item(source)
+ if (!istype(shoes) || !shoes.can_be_tied)
+ return null
+
+ switch (shoes.tied)
+ if (SHOES_UNTIED)
+ return "knot"
+ if (SHOES_TIED)
+ return "untie"
+ if (SHOES_KNOTTED)
+ return "unknot"
+
+/datum/strippable_item/mob_item_slot/feet/alternate_action(atom/source, mob/user)
+ var/obj/item/clothing/shoes/shoes = get_item(source)
+ if (!istype(shoes))
+ return
+
+ shoes.handle_tying(user)
+
+/datum/strippable_item/mob_item_slot/suit_storage
+ key = STRIPPABLE_ITEM_SUIT_STORAGE
+ item_slot = ITEM_SLOT_SUITSTORE
+
+/datum/strippable_item/mob_item_slot/suit_storage/get_alternate_action(atom/source, mob/user)
+ return get_strippable_alternate_action_internals(get_item(source), source)
+
+/datum/strippable_item/mob_item_slot/suit_storage/alternate_action(atom/source, mob/user)
+ return strippable_alternate_action_internals(get_item(source), source, user)
+
+/datum/strippable_item/mob_item_slot/id
+ key = STRIPPABLE_ITEM_ID
+ item_slot = ITEM_SLOT_ID
+
+/datum/strippable_item/mob_item_slot/belt
+ key = STRIPPABLE_ITEM_BELT
+ item_slot = ITEM_SLOT_BELT
+
+/datum/strippable_item/mob_item_slot/belt/get_alternate_action(atom/source, mob/user)
+ return get_strippable_alternate_action_internals(get_item(source), source)
+
+/datum/strippable_item/mob_item_slot/belt/alternate_action(atom/source, mob/user)
+ return strippable_alternate_action_internals(get_item(source), source, user)
+
+/datum/strippable_item/mob_item_slot/pocket
+ /// Which pocket we're referencing. Used for visible text.
+ var/pocket_side
+
+/datum/strippable_item/mob_item_slot/pocket/get_obscuring(atom/source)
+ return isnull(get_item(source)) \
+ ? STRIPPABLE_OBSCURING_NONE \
+ : STRIPPABLE_OBSCURING_HIDDEN
+
+/datum/strippable_item/mob_item_slot/pocket/get_equip_delay(obj/item/equipping)
+ return POCKET_EQUIP_DELAY
+
+/datum/strippable_item/mob_item_slot/pocket/start_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ warn_owner(source)
+
+/datum/strippable_item/mob_item_slot/pocket/start_unequip(atom/source, mob/user)
+ var/obj/item/item = get_item(source)
+ if (isnull(item))
+ return FALSE
+
+ to_chat(user, "You try to empty [source]'s [pocket_side] pocket.")
+
+ var/log_message = "[key_name(source)] is being pickpocketed of [item] by [key_name(user)] ([pocket_side])"
+ source.log_message(log_message, LOG_ATTACK, color="red")
+ user.log_message(log_message, LOG_ATTACK, color="red", log_globally=FALSE)
+ item.add_fingerprint(src)
+
+ var/result = start_unequip_mob(item, source, user, POCKET_STRIP_DELAY)
+
+ if (!result)
+ warn_owner(source)
+
+ return result
+
+/datum/strippable_item/mob_item_slot/pocket/proc/warn_owner(atom/owner)
+ to_chat(owner, "You feel your [pocket_side] pocket being fumbled with!")
+
+/datum/strippable_item/mob_item_slot/pocket/left
+ key = STRIPPABLE_ITEM_LPOCKET
+ item_slot = ITEM_SLOT_LPOCKET
+ pocket_side = "left"
+
+/datum/strippable_item/mob_item_slot/pocket/right
+ key = STRIPPABLE_ITEM_RPOCKET
+ item_slot = ITEM_SLOT_RPOCKET
+ pocket_side = "right"
+
+/proc/get_strippable_alternate_action_internals(obj/item/item, atom/source)
+ if (!iscarbon(source))
+ return
+
+ var/mob/living/carbon/carbon_source = source
+
+ var/obj/item/clothing/mask = carbon_source.wear_mask
+ if (!istype(mask))
+ return
+
+ if ((mask.clothing_flags & MASKINTERNALS) && istype(item, /obj/item/tank))
+ return isnull(carbon_source.internal) ? "enable_internals" : "disable_internals"
+
+/proc/strippable_alternate_action_internals(obj/item/item, atom/source, mob/user)
+ var/obj/item/tank/tank = item
+ if (!istype(tank))
+ return
+
+ var/mob/living/carbon/carbon_source = source
+ if (!istype(carbon_source))
+ return
+
+ var/obj/item/clothing/mask = carbon_source.wear_mask
+ if (!istype(mask) || !(mask.clothing_flags & MASKINTERNALS))
+ return
+
+ carbon_source.visible_message(
+ "[user] tries to [isnull(carbon_source.internal) ? "open": "close"] the valve on [source]'s [item.name].",
+ "[user] tries to [isnull(carbon_source.internal) ? "open": "close"] the valve on your [item.name].",
+ ignored_mobs = user,
+ )
+
+ to_chat(user, "You try to [isnull(carbon_source.internal) ? "open": "close"] the valve on [source]'s [item.name]...")
+
+ if(!do_mob(user, carbon_source, INTERNALS_TOGGLE_DELAY))
+ return
+
+ if(carbon_source.internal)
+ carbon_source.internal = null
+
+ // This isn't meant to be FALSE, it correlates to the icon's name.
+ carbon_source.update_internals_hud_icon(0)
+ else if (!QDELETED(item))
+ if((carbon_source.wear_mask?.clothing_flags & MASKINTERNALS) || carbon_source.getorganslot(ORGAN_SLOT_BREATHING_TUBE))
+ carbon_source.internal = item
+ carbon_source.update_internals_hud_icon(1)
+
+ carbon_source.visible_message(
+ "[user] [isnull(carbon_source.internal) ? "closes": "opens"] the valve on [source]'s [item.name].",
+ "[user] [isnull(carbon_source.internal) ? "closes": "opens"] the valve on your [item.name].",
+ ignored_mobs = user,
+ )
+
+ to_chat(user, "You [isnull(carbon_source.internal) ? "close" : "open"] the valve on [source]'s [item.name].")
+
+#undef INTERNALS_TOGGLE_DELAY
+#undef POCKET_EQUIP_DELAY
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 11919c51e8b..180da8f5cfa 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1012,13 +1012,6 @@
who.log_message("[key_name(who)] has been stripped of [what] by [key_name(src)]", LOG_ATTACK, color="red")
log_message("[key_name(who)] has been stripped of [what] by [key_name(src)]", LOG_ATTACK, color="red", log_globally=FALSE)
- if(Adjacent(who)) //update inventory window
- who.show_inv(src)
- else
- src << browse(null,"window=mob[REF(who)]")
-
- who.update_equipment_speed_mods() // Updates speed in case stripped speed affecting item
-
// The src mob is trying to place an item on someone
// Override if a certain mob should be behave differently when placing items (can't, for example)
/mob/living/stripPanelEquip(obj/item/what, mob/who, where)
@@ -1061,11 +1054,6 @@
who.log_message("[key_name(who)] had [what] put on them by [key_name(src)]", LOG_ATTACK, color="red")
log_message("[key_name(who)] had [what] put on them by [key_name(src)]", LOG_ATTACK, color="red", log_globally=FALSE)
- if(Adjacent(who)) //update inventory window
- who.show_inv(src)
- else
- src << browse(null,"window=mob[REF(who)]")
-
/mob/living/singularity_pull(S, current_size)
..()
if(move_resist == INFINITY)
diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm
index 316b590db65..6ff661e797d 100644
--- a/code/modules/mob/living/simple_animal/friendly/dog.dm
+++ b/code/modules/mob/living/simple_animal/friendly/dog.dm
@@ -135,6 +135,7 @@
/mob/living/simple_animal/pet/dog/corgi/Initialize()
. = ..()
regenerate_icons()
+ AddElement(/datum/element/strippable, GLOB.strippable_corgi_items)
/mob/living/simple_animal/pet/dog/corgi/exoticcorgi/Initialize()
. = ..()
@@ -145,24 +146,163 @@
..(gibbed)
regenerate_icons()
-/mob/living/simple_animal/pet/dog/corgi/show_inv(mob/user)
- if(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
+GLOBAL_LIST_INIT(strippable_corgi_items, create_strippable_list(list(
+ /datum/strippable_item/corgi_head,
+ /datum/strippable_item/corgi_back,
+ /datum/strippable_item/corgi_collar,
+ /datum/strippable_item/corgi_id,
+)))
+
+/datum/strippable_item/corgi_head
+ key = STRIPPABLE_ITEM_HEAD
+
+/datum/strippable_item/corgi_head/get_item(atom/source)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
return
- user.set_machine(src)
- var/list/dat = list()
- dat += "
- Close
- "}
+ return corgi_source.inventory_head
- var/datum/browser/popup = new(user, "mob[REF(src)]", "[src]", 440, 510)
- popup.set_content(dat.Join())
- popup.open()
+/datum/strippable_item/corgi_head/finish_equip(atom/source, obj/item/equipping, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ corgi_source.place_on_head(equipping, user)
+
+/datum/strippable_item/corgi_head/finish_unequip(atom/source, obj/item/equipping, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ user.put_in_hands(corgi_source.inventory_head)
+ corgi_source.inventory_head = null
+ corgi_source.update_corgi_fluff()
+ corgi_source.regenerate_icons()
+
+/datum/strippable_item/corgi_back
+ key = STRIPPABLE_ITEM_BACK
+
+/datum/strippable_item/corgi_back/get_item(atom/source)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ return corgi_source.inventory_back
+
+/datum/strippable_item/corgi_back/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ if (!ispath(equipping.dog_fashion, /datum/dog_fashion/back))
+ to_chat(user, "You set [equipping] on [source]'s back, but it falls off!")
+ equipping.forceMove(source.drop_location())
+ if (prob(25))
+ step_rand(equipping)
+ dance_rotate(source, set_original_dir = TRUE)
+
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/corgi_back/finish_equip(atom/source, obj/item/equipping, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ equipping.forceMove(corgi_source)
+ corgi_source.inventory_back = equipping
+ corgi_source.update_corgi_fluff()
+ corgi_source.regenerate_icons()
+
+/datum/strippable_item/corgi_back/finish_unequip(atom/source, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ user.put_in_hands(corgi_source.inventory_back)
+ corgi_source.inventory_back = null
+ corgi_source.update_corgi_fluff()
+ corgi_source.regenerate_icons()
+
+/datum/strippable_item/corgi_collar
+ key = STRIPPABLE_ITEM_CORGI_COLLAR
+
+/datum/strippable_item/corgi_collar/get_item(atom/source)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ return corgi_source.pcollar
+
+/datum/strippable_item/corgi_collar/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ if (!istype(equipping, /obj/item/clothing/neck/petcollar))
+ to_chat(user, "That's not a collar.")
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/corgi_collar/finish_equip(atom/source, obj/item/equipping, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ corgi_source.add_collar(equipping, user)
+ corgi_source.update_corgi_fluff()
+
+/datum/strippable_item/corgi_collar/finish_unequip(atom/source, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ user.put_in_hands(corgi_source.pcollar)
+ corgi_source.pcollar = null
+ corgi_source.update_corgi_fluff()
+ corgi_source.regenerate_icons()
+
+/datum/strippable_item/corgi_id
+ key = STRIPPABLE_ITEM_ID
+
+/datum/strippable_item/corgi_id/get_item(atom/source)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ return corgi_source.access_card
+
+/datum/strippable_item/corgi_id/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ if (!istype(equipping, /obj/item/card/id))
+ to_chat(user, "You can't pin [equipping] to [source]!")
+ return FALSE
+
+ return TRUE
+
+/datum/strippable_item/corgi_id/finish_equip(atom/source, obj/item/equipping, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ equipping.forceMove(source)
+ corgi_source.access_card = equipping
+
+/datum/strippable_item/corgi_id/finish_unequip(atom/source, mob/user)
+ var/mob/living/simple_animal/pet/dog/corgi/corgi_source = source
+ if (!istype(corgi_source))
+ return
+
+ user.put_in_hands(corgi_source.access_card)
+ corgi_source.access_card = null
+ corgi_source.update_corgi_fluff()
+ corgi_source.regenerate_icons()
/mob/living/simple_animal/pet/dog/corgi/getarmor(def_zone, type)
var/armorval = 0
@@ -205,128 +345,12 @@
..()
update_corgi_fluff()
-/mob/living/simple_animal/pet/dog/corgi/Topic(href, href_list)
- if(!(iscarbon(usr) || iscyborg(usr)) || !usr.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
- usr << browse(null, "window=mob[REF(src)]")
- usr.unset_machine()
- return
-
- //Removing from inventory
- if(href_list["remove_inv"])
- var/remove_from = href_list["remove_inv"]
- switch(remove_from)
- if(BODY_ZONE_HEAD)
- if(inventory_head)
- usr.put_in_hands(inventory_head)
- inventory_head = null
- update_corgi_fluff()
- regenerate_icons()
- else
- to_chat(usr, "There is nothing to remove from its [remove_from]!")
- return
- if("back")
- if(inventory_back)
- usr.put_in_hands(inventory_back)
- inventory_back = null
- update_corgi_fluff()
- regenerate_icons()
- else
- to_chat(usr, "There is nothing to remove from its [remove_from]!")
- return
- if("collar")
- if(pcollar)
- usr.put_in_hands(pcollar)
- pcollar = null
- update_corgi_fluff()
- regenerate_icons()
- if("card")
- if(access_card)
- usr.put_in_hands(access_card)
- access_card = null
-
- show_inv(usr)
-
- //Adding things to inventory
- else if(href_list["add_inv"])
- var/add_to = href_list["add_inv"]
-
- switch(add_to)
- if("collar")
- var/obj/item/clothing/neck/petcollar/P = usr.get_active_held_item()
- if(!istype(P))
- to_chat(usr,"That's not a collar.")
- return
- add_collar(P, usr)
- update_corgi_fluff()
-
- if(BODY_ZONE_HEAD)
- place_on_head(usr.get_active_held_item(),usr)
-
- if("back")
- if(inventory_back)
- to_chat(usr, "It's already wearing something!")
- return
- else
- var/obj/item/item_to_add = usr.get_active_held_item()
-
- if(!item_to_add)
- usr.visible_message("[usr] pets [src].", "You rest your hand on [src]'s back for a moment.")
- return
-
- if(!usr.temporarilyRemoveItemFromInventory(item_to_add))
- to_chat(usr, "\The [item_to_add] is stuck to your hand, you cannot put it on [src]'s back!")
- return
-
- if(istype(item_to_add, /obj/item/grenade/c4)) // last thing he ever wears, I guess
- item_to_add.afterattack(src,usr,1)
- return
-
- //The objects that corgis can wear on their backs.
- var/allowed = FALSE
- if(ispath(item_to_add.dog_fashion, /datum/dog_fashion/back))
- allowed = TRUE
-
- if(!allowed)
- to_chat(usr, "You set [item_to_add] on [src]'s back, but it falls off!")
- item_to_add.forceMove(drop_location())
- if(prob(25))
- step_rand(item_to_add)
- dance_rotate(src, set_original_dir=TRUE)
- return
-
- item_to_add.forceMove(src)
- inventory_back = item_to_add
- update_corgi_fluff()
- regenerate_icons()
- if("card")
- if(access_card)
- to_chat(usr, "[src] already has \an [access_card] pinned to [p_them()]!")
- return
- var/obj/item/item_to_add = usr.get_active_held_item()
- if(!usr.temporarilyRemoveItemFromInventory(item_to_add))
- to_chat(usr, "\The [item_to_add] is stuck to your hand, you cannot pin it to [src]!")
- return
- if(!istype(item_to_add, /obj/item/card/id))
- to_chat(usr, "You can't pin [item_to_add] to [src]!")
- return
- item_to_add.forceMove(src)
- access_card = item_to_add
-
- show_inv(usr)
- else
- return ..()
-
//Corgis are supposed to be simpler, so only a select few objects can actually be put
//to be compatible with them. The objects are below.
//Many hats added, Some will probably be removed, just want to see which ones are popular.
// > some will probably be removed
/mob/living/simple_animal/pet/dog/corgi/proc/place_on_head(obj/item/item_to_add, mob/user)
-
- if(istype(item_to_add, /obj/item/grenade/c4)) // last thing he ever wears, I guess
- INVOKE_ASYNC(item_to_add, /obj/item.proc/afterattack, src, user, 1)
- return
-
if(inventory_head)
if(user)
to_chat(user, "You can't put more than one hat on [src]!")
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index e9a4bf38e77..b0c9c3aaf11 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -128,6 +128,7 @@
/mob/living/simple_animal/parrot/proc/toggle_mode,
/mob/living/simple_animal/parrot/proc/perch_mob_player))
+ AddElement(/datum/element/strippable, GLOB.strippable_parrot_items)
/mob/living/simple_animal/parrot/examine(mob/user)
. = ..()
@@ -185,99 +186,96 @@
return FALSE
-/*
- * Inventory
- */
-/mob/living/simple_animal/parrot/show_inv(mob/user)
- user.set_machine(src)
- var/list/dat = list()
+GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
+ /datum/strippable_item/parrot_headset,
+)))
- dat += "
- Close
- "}
+/datum/strippable_item/parrot_headset
+ key = STRIPPABLE_ITEM_PARROT_HEADSET
- var/datum/browser/popup = new(user, "mob[REF(src)]", "[src]", 440, 510)
- popup.set_content(dat.Join())
- popup.open()
+/datum/strippable_item/parrot_headset/get_item(atom/source)
+ var/mob/living/simple_animal/parrot/parrot_source = source
+ return istype(parrot_source) ? parrot_source.ears : null
+/datum/strippable_item/parrot_headset/try_equip(atom/source, obj/item/equipping, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
-/mob/living/simple_animal/parrot/Topic(href, href_list)
- if(!(iscarbon(usr) || iscyborg(usr)) || !usr.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
- usr << browse(null, "window=mob[REF(src)]")
- usr.unset_machine()
+ if (!istype(equipping, /obj/item/radio/headset))
+ to_chat(user, "[equipping] won't fit!")
+ return FALSE
+
+ return TRUE
+
+// There is no delay for putting a headset on a parrot.
+/datum/strippable_item/parrot_headset/start_equip(atom/source, obj/item/equipping, mob/user)
+ return TRUE
+
+/datum/strippable_item/parrot_headset/finish_equip(atom/source, obj/item/equipping, mob/user)
+ var/obj/item/radio/headset/radio = equipping
+ if (!istype(radio))
return
- //Removing from inventory
- if(href_list["remove_inv"])
- var/remove_from = href_list["remove_inv"]
- switch(remove_from)
- if("ears")
- if(!ears)
- to_chat(usr, "There is nothing to remove from its [remove_from]!")
- return
- if(!stat)
- say("[available_channels.len ? "[pick(available_channels)] " : null]BAWWWWWK LEAVE THE HEADSET BAWKKKKK!")
- ears.forceMove(drop_location())
- ears = null
- for(var/possible_phrase in speak)
- if(copytext_char(possible_phrase, 2, 3) in GLOB.department_radio_keys)
- possible_phrase = copytext_char(possible_phrase, 3)
- show_inv(usr)
+ var/mob/living/simple_animal/parrot/parrot_source = source
+ if (!istype(parrot_source))
+ return
- //Adding things to inventory
- else if(href_list["add_inv"])
- var/add_to = href_list["add_inv"]
- if(!usr.get_active_held_item())
- to_chat(usr, "You have nothing in your hand to put on its [add_to]!")
- return
- switch(add_to)
- if("ears")
- if(ears)
- to_chat(usr, "It's already wearing something!")
- return
- else
- var/obj/item/item_to_add = usr.get_active_held_item()
- if(!item_to_add)
- return
+ if (!user.transferItemToLoc(radio, source))
+ return
- if( !istype(item_to_add, /obj/item/radio/headset) )
- to_chat(usr, "This object won't fit!")
- return
+ parrot_source.ears = radio
- var/obj/item/radio/headset/headset_to_add = item_to_add
+ to_chat(user, "You fit [radio] onto [source].")
- if(!usr.transferItemToLoc(headset_to_add, src))
- return
- ears = headset_to_add
- to_chat(usr, "You fit the headset onto [src].")
+ parrot_source.available_channels.Cut()
- available_channels.Cut()
- for(var/ch in headset_to_add.channels)
- switch(ch)
- if(RADIO_CHANNEL_ENGINEERING)
- available_channels.Add(RADIO_TOKEN_ENGINEERING)
- if(RADIO_CHANNEL_COMMAND)
- available_channels.Add(RADIO_TOKEN_COMMAND)
- if(RADIO_CHANNEL_SECURITY)
- available_channels.Add(RADIO_TOKEN_SECURITY)
- if(RADIO_CHANNEL_SCIENCE)
- available_channels.Add(RADIO_TOKEN_SCIENCE)
- if(RADIO_CHANNEL_MEDICAL)
- available_channels.Add(RADIO_TOKEN_MEDICAL)
- if(RADIO_CHANNEL_SUPPLY)
- available_channels.Add(RADIO_TOKEN_SUPPLY)
- if(RADIO_CHANNEL_SERVICE)
- available_channels.Add(RADIO_TOKEN_SERVICE)
+ for (var/channel in radio.channels)
+ var/channel_to_add
- if(headset_to_add.translate_binary)
- available_channels.Add(MODE_TOKEN_BINARY)
- show_inv(usr)
+ switch (channel)
+ if (RADIO_CHANNEL_ENGINEERING)
+ channel_to_add = RADIO_TOKEN_ENGINEERING
+ if (RADIO_CHANNEL_COMMAND)
+ channel_to_add = RADIO_TOKEN_COMMAND
+ if (RADIO_CHANNEL_SECURITY)
+ channel_to_add = RADIO_TOKEN_SECURITY
+ if (RADIO_CHANNEL_SCIENCE)
+ channel_to_add = RADIO_TOKEN_SCIENCE
+ if (RADIO_CHANNEL_MEDICAL)
+ channel_to_add = RADIO_TOKEN_MEDICAL
+ if (RADIO_CHANNEL_SUPPLY)
+ channel_to_add = RADIO_TOKEN_SUPPLY
+ if (RADIO_CHANNEL_SERVICE)
+ channel_to_add = RADIO_TOKEN_SERVICE
- else
- return ..()
+ if (channel_to_add)
+ parrot_source.available_channels += channel_to_add
+ if (radio.translate_binary)
+ parrot_source.available_channels.Add(MODE_TOKEN_BINARY)
+
+/datum/strippable_item/parrot_headset/start_unequip(atom/source, mob/user)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ var/mob/living/simple_animal/parrot/parrot_source = source
+ if (!istype(parrot_source))
+ return
+
+ if (!parrot_source.stat)
+ parrot_source.say("[parrot_source.available_channels.len ? "[pick(parrot_source.available_channels)] " : null]BAWWWWWK LEAVE THE HEADSET BAWKKKKK!")
+
+ return TRUE
+
+/datum/strippable_item/parrot_headset/finish_unequip(atom/source, mob/user)
+ var/mob/living/simple_animal/parrot/parrot_source = source
+ if (!istype(parrot_source))
+ return
+
+ parrot_source.ears.forceMove(parrot_source.drop_location())
+ parrot_source.ears = null
/*
* Attack responces
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 01c3eb78342..16c6b278622 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -426,11 +426,6 @@
client.eye = loc
return 1
-/// Show the mob's inventory to another mob
-/mob/proc/show_inv(mob/user)
- return
-
-
/**
* Examine a mob
*
@@ -775,12 +770,6 @@
else
user.stripPanelEquip(what,src,slot)
- if(user.machine == src)
- if(Adjacent(user))
- show_inv(user)
- else
- user << browse(null,"window=mob[REF(src)]")
-
// The src mob is trying to strip an item from someone
// Defined in living.dm
/mob/proc/stripPanelUnequip(obj/item/what, mob/who)
@@ -804,28 +793,6 @@
return
if(isAI(M))
return
-/**
- * Handle the result of a click drag onto this mob
- *
- * For mobs this just shows the inventory
- */
-/mob/MouseDrop_T(atom/dropping, atom/user)
- . = ..()
-
- // Our mouse drop has already been handled by something else. Most likely buckling code.
- // Since it has already been handled, we don't need to show inventory.
- if(.)
- return
-
- if(ismob(dropping) && src == user && dropping != user)
- var/mob/M = dropping
- var/mob/U = user
- if(iscyborg(U))
- var/mob/living/silicon/robot/cyborg = U
- if(cyborg.combat_mode)
- M.show_inv(cyborg)
- else
- M.show_inv(U)
///Is the mob muzzled (default false)
/mob/proc/is_muzzled()
diff --git a/icons/UI_Icons/inventory/back.png b/icons/UI_Icons/inventory/back.png
new file mode 100644
index 00000000000..736b9d64bf9
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 00000000000..1be89d450a8
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 00000000000..71803b1b6c6
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 00000000000..e9a8f3c23c4
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 00000000000..6e6f1ad098f
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 00000000000..2c8a16cbdb7
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 00000000000..b09228d65f6
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 00000000000..0e05a487e07
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 00000000000..11e2d2254cd
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 00000000000..4469591d36f
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 00000000000..82e51089379
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 00000000000..78ad3ce3b1c
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 00000000000..f42399dca0f
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 00000000000..d20f7ef4d10
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 00000000000..e9c48e8069f
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 00000000000..9722eb10297
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 00000000000..292b3324b5b
Binary files /dev/null and b/icons/UI_Icons/inventory/uniform.png differ
diff --git a/tgstation.dme b/tgstation.dme
index 61fec2b0685..4a4efb29e06 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -127,6 +127,7 @@
#include "code\__DEFINES\statpanel.dm"
#include "code\__DEFINES\status_effects.dm"
#include "code\__DEFINES\storage.dm"
+#include "code\__DEFINES\strippable.dm"
#include "code\__DEFINES\subsystems.dm"
#include "code\__DEFINES\supermatter.dm"
#include "code\__DEFINES\tgs.config.dm"
@@ -668,6 +669,7 @@
#include "code\datums\elements\snail_crawl.dm"
#include "code\datums\elements\squashable.dm"
#include "code\datums\elements\squish.dm"
+#include "code\datums\elements\strippable.dm"
#include "code\datums\elements\swabbable.dm"
#include "code\datums\elements\tool_flash.dm"
#include "code\datums\elements\turf_transparency.dm"
@@ -2447,6 +2449,7 @@
#include "code\modules\mob\living\carbon\carbon_defines.dm"
#include "code\modules\mob\living\carbon\carbon_movement.dm"
#include "code\modules\mob\living\carbon\carbon_say.dm"
+#include "code\modules\mob\living\carbon\carbon_stripping.dm"
#include "code\modules\mob\living\carbon\carbon_update_icons.dm"
#include "code\modules\mob\living\carbon\damage_procs.dm"
#include "code\modules\mob\living\carbon\death.dm"
@@ -2501,6 +2504,7 @@
#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_say.dm"
+#include "code\modules\mob\living\carbon\human\human_stripping.dm"
#include "code\modules\mob\living\carbon\human\human_update_icons.dm"
#include "code\modules\mob\living\carbon\human\inventory.dm"
#include "code\modules\mob\living\carbon\human\life.dm"
diff --git a/tgui/packages/common/collections.spec.ts b/tgui/packages/common/collections.spec.ts
new file mode 100644
index 00000000000..58eff7f354c
--- /dev/null
+++ b/tgui/packages/common/collections.spec.ts
@@ -0,0 +1,20 @@
+import { range, zip } from "./collections";
+
+// Type assertions, these will lint if the types are wrong.
+const _zip1: [string, number] = zip(["a"], [1])[0];
+
+describe("range", () => {
+ test("range(0, 5)", () => {
+ expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
+ });
+});
+
+describe("zip", () => {
+ test("zip(['a', 'b', 'c'], [1, 2, 3, 4])", () => {
+ expect(zip(["a", "b", "c"], [1, 2, 3, 4])).toEqual([
+ ["a", 1],
+ ["b", 2],
+ ["c", 3],
+ ]);
+ });
+});
diff --git a/tgui/packages/common/collections.js b/tgui/packages/common/collections.ts
similarity index 91%
rename from tgui/packages/common/collections.js
rename to tgui/packages/common/collections.ts
index 5c0b3d08938..7abe49ff238 100644
--- a/tgui/packages/common/collections.js
+++ b/tgui/packages/common/collections.ts
@@ -173,6 +173,13 @@ export const sortBy = (...iterateeFns) => array => {
export const sort = sortBy();
+/**
+ * Returns a range of numbers from start to end, exclusively.
+ * For example, range(0, 5) will return [0, 1, 2, 3, 4].
+ */
+export const range = (start: number, end: number): number[] =>
+ new Array(end - start).fill(null).map((_, index) => index + start);
+
/**
* A fast implementation of reduce.
*/
@@ -205,14 +212,17 @@ export const reduce = (reducerFn, initialValue) => array => {
* is determined by the order they occur in the array. The iteratee is
* invoked with one argument: value.
*/
-export const uniqBy = iterateeFn => array => {
+/* eslint-disable indent */
+export const uniqBy = (
+ iterateeFn?: (value: T) => unknown
+) => (array: T[]) => {
const { length } = array;
const result = [];
const seen = iterateeFn ? [] : result;
let index = -1;
outer:
while (++index < length) {
- let value = array[index];
+ let value: T | 0 = array[index];
const computed = iterateeFn ? iterateeFn(value) : value;
value = value !== 0 ? value : 0;
if (computed === computed) {
@@ -236,17 +246,20 @@ export const uniqBy = iterateeFn => array => {
}
return result;
};
+/* eslint-enable indent */
export const uniq = uniqBy();
+type Zip = {
+ [I in keyof T]: T[I] extends (infer U)[] ? U : never;
+}[];
+
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
* the second elements of the given arrays, and so on.
- *
- * @returns {any[]}
*/
-export const zip = (...arrays) => {
+export const zip = (...arrays: T): Zip => {
if (arrays.length === 0) {
return;
}
diff --git a/tgui/packages/common/math.js b/tgui/packages/common/math.ts
similarity index 98%
rename from tgui/packages/common/math.js
rename to tgui/packages/common/math.ts
index df7e395ea79..97e6b60b2ed 100644
--- a/tgui/packages/common/math.js
+++ b/tgui/packages/common/math.ts
@@ -45,7 +45,7 @@ export const round = (value, precision) => {
m = Math.pow(10, precision);
value *= m;
// sign of the number
- sgn = (value > 0) | -(value < 0);
+ sgn = +(value > 0) | -(value < 0);
// isHalf = value % 1 === 0.5 * sgn;
isHalf = Math.abs(value % 1) >= 0.4999999999854481;
f = Math.floor(value);
diff --git a/tgui/packages/tgfont/icons/air-tank-slash.svg b/tgui/packages/tgfont/icons/air-tank-slash.svg
new file mode 100644
index 00000000000..c72fca54fae
--- /dev/null
+++ b/tgui/packages/tgfont/icons/air-tank-slash.svg
@@ -0,0 +1,29 @@
+
+
+
diff --git a/tgui/packages/tgfont/icons/air-tank.svg b/tgui/packages/tgfont/icons/air-tank.svg
new file mode 100644
index 00000000000..2ca048e0197
--- /dev/null
+++ b/tgui/packages/tgfont/icons/air-tank.svg
@@ -0,0 +1,26 @@
+
+
+
diff --git a/tgui/packages/tgfont/icons/nanotrasen_logo.svg b/tgui/packages/tgfont/icons/nanotrasen-logo.svg
similarity index 100%
rename from tgui/packages/tgfont/icons/nanotrasen_logo.svg
rename to tgui/packages/tgfont/icons/nanotrasen-logo.svg
diff --git a/tgui/packages/tgfont/icons/syndicate_logo.svg b/tgui/packages/tgfont/icons/syndicate-logo.svg
similarity index 100%
rename from tgui/packages/tgfont/icons/syndicate_logo.svg
rename to tgui/packages/tgfont/icons/syndicate-logo.svg
diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx
new file mode 100644
index 00000000000..3832ca087c5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/StripMenu.tsx
@@ -0,0 +1,418 @@
+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: {
+ align: "left" | "right";
+ children: string;
+}): JSX.Element => {
+ const { align, children } = props;
+
+ return (
+
+ {children}
+
+ );
+};
+
+type AlternateAction = {
+ icon: string;
+ text: string;
+};
+
+const ALTERNATE_ACTIONS: Record = {
+ knot: {
+ icon: "shoe-prints",
+ text: "Knot",
+ },
+
+ untie: {
+ icon: "shoe-prints",
+ text: "Untie",
+ },
+
+ unknot: {
+ icon: "shoe-prints",
+ text: "Unknot",
+ },
+
+ enable_internals: {
+ icon: "tg-air-tank",
+ text: "Enable internals",
+ },
+
+ disable_internals: {
+ icon: "tg-air-tank-slash",
+ text: "Disable internals",
+ },
+
+ adjust_jumpsuit: {
+ icon: "tshirt",
+ text: "Adjust jumpsuit",
+ },
+};
+
+const SLOTS: Record<
+ string,
+ {
+ displayName: string;
+ gridSpot: GridSpotKey;
+ image?: string;
+ additionalComponent?: JSX.Element;
+ }
+> = {
+ eyes: {
+ displayName: "eyewear",
+ gridSpot: getGridSpotKey([0, 1]),
+ image: "inventory-glasses.png",
+ },
+
+ head: {
+ displayName: "headwear",
+ gridSpot: getGridSpotKey([0, 2]),
+ image: "inventory-head.png",
+ },
+
+ neck: {
+ displayName: "neckwear",
+ gridSpot: getGridSpotKey([1, 1]),
+ image: "inventory-neck.png",
+ },
+
+ mask: {
+ displayName: "mask",
+ gridSpot: getGridSpotKey([1, 2]),
+ image: "inventory-mask.png",
+ },
+
+ corgi_collar: {
+ displayName: "collar",
+ gridSpot: getGridSpotKey([1, 2]),
+ image: "inventory-collar.png",
+ },
+
+ ears: {
+ displayName: "earwear",
+ gridSpot: getGridSpotKey([1, 3]),
+ image: "inventory-ears.png",
+ },
+
+ parrot_headset: {
+ displayName: "headset",
+ gridSpot: getGridSpotKey([1, 3]),
+ image: "inventory-ears.png",
+ },
+
+ handcuffs: {
+ displayName: "handcuffs",
+ gridSpot: getGridSpotKey([1, 4]),
+ },
+
+ legcuffs: {
+ displayName: "legcuffs",
+ gridSpot: getGridSpotKey([1, 5]),
+ },
+
+ jumpsuit: {
+ displayName: "uniform",
+ gridSpot: getGridSpotKey([2, 1]),
+ image: "inventory-uniform.png",
+ },
+
+ suit: {
+ displayName: "suit",
+ gridSpot: getGridSpotKey([2, 2]),
+ image: "inventory-suit.png",
+ },
+
+ gloves: {
+ displayName: "gloves",
+ gridSpot: getGridSpotKey([2, 3]),
+ image: "inventory-gloves.png",
+ },
+
+ right_hand: {
+ displayName: "right hand",
+ gridSpot: getGridSpotKey([2, 4]),
+ image: "inventory-hand_r.png",
+ additionalComponent: R,
+ },
+
+ left_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",
+ },
+
+ suit_storage: {
+ 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",
+ },
+
+ left_pocket: {
+ displayName: "left pocket",
+ gridSpot: getGridSpotKey([4, 4]),
+ image: "inventory-pocket.png",
+ },
+
+ right_pocket: {
+ 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;
+ }
+ ) &
+ Partial);
+
+type StripMenuData = {
+ items: Record;
+ name: string;
+};
+
+export const StripMenu = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const gridSpots = new Map();
+ for (const key of Object.keys(data.items)) {
+ 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}`;
+ }
+
+ return (
+
+
+
+
+ {alternateAction !== undefined && (
+
+ )}
+
+
+ );
+ })}
+
+
+ ))}
+
+
+
+ );
+};