From f71b3a069b4f80a2bbce47bcb1f8c178805a9a30 Mon Sep 17 00:00:00 2001 From: Mothblocks <35135081+Mothblocks@users.noreply.github.com> Date: Fri, 26 Mar 2021 02:33:46 -0700 Subject: [PATCH] tgui strip panel + strippable element (#57889) Completely removes show_inv and replaces it with /datum/element/strippable. It takes a list of instantiated /datum/strippable_item which communicate which slots are available and how to interact with them. This element has been added to humans, alien humanoids, parrots, and corgis. Co-authored-by: Aleksej Komarov --- code/__DEFINES/mobs.dm | 2 +- code/__DEFINES/strippable.dm | 31 ++ code/__HELPERS/_lists.dm | 8 +- code/datums/elements/strippable.dm | 488 ++++++++++++++++++ code/game/objects/items.dm | 2 +- code/modules/asset_cache/asset_list_items.dm | 21 + code/modules/clothing/neck/_neck.dm | 2 +- code/modules/clothing/shoes/_shoes.dm | 7 + code/modules/clothing/spacesuits/hardsuit.dm | 2 +- .../living/carbon/alien/humanoid/humanoid.dm | 47 +- .../mob/living/carbon/alien/larva/larva.dm | 4 - code/modules/mob/living/carbon/carbon.dm | 62 --- .../mob/living/carbon/carbon_stripping.dm | 129 +++++ code/modules/mob/living/carbon/human/human.dm | 158 +----- .../living/carbon/human/human_stripping.dm | 232 +++++++++ code/modules/mob/living/living.dm | 12 - .../mob/living/simple_animal/friendly/dog.dm | 286 +++++----- .../mob/living/simple_animal/parrot.dm | 156 +++--- code/modules/mob/mob.dm | 33 -- icons/UI_Icons/inventory/back.png | Bin 0 -> 1796 bytes icons/UI_Icons/inventory/belt.png | Bin 0 -> 1596 bytes icons/UI_Icons/inventory/collar.png | Bin 0 -> 2090 bytes icons/UI_Icons/inventory/ears.png | Bin 0 -> 1688 bytes icons/UI_Icons/inventory/glasses.png | Bin 0 -> 2017 bytes icons/UI_Icons/inventory/gloves.png | Bin 0 -> 2107 bytes icons/UI_Icons/inventory/hand_l.png | Bin 0 -> 2019 bytes icons/UI_Icons/inventory/hand_r.png | Bin 0 -> 2017 bytes icons/UI_Icons/inventory/head.png | Bin 0 -> 1854 bytes icons/UI_Icons/inventory/id.png | Bin 0 -> 1940 bytes icons/UI_Icons/inventory/mask.png | Bin 0 -> 1930 bytes icons/UI_Icons/inventory/neck.png | Bin 0 -> 1852 bytes icons/UI_Icons/inventory/pocket.png | Bin 0 -> 1831 bytes icons/UI_Icons/inventory/shoes.png | Bin 0 -> 1817 bytes icons/UI_Icons/inventory/suit.png | Bin 0 -> 2073 bytes icons/UI_Icons/inventory/suit_storage.png | Bin 0 -> 1976 bytes icons/UI_Icons/inventory/uniform.png | Bin 0 -> 1885 bytes tgstation.dme | 4 + tgui/packages/common/collections.spec.ts | 20 + .../common/{collections.js => collections.ts} | 23 +- tgui/packages/common/{math.js => math.ts} | 2 +- tgui/packages/tgfont/icons/air-tank-slash.svg | 29 ++ tgui/packages/tgfont/icons/air-tank.svg | 26 + ...anotrasen_logo.svg => nanotrasen-logo.svg} | 0 ...{syndicate_logo.svg => syndicate-logo.svg} | 0 tgui/packages/tgui/interfaces/StripMenu.tsx | 418 +++++++++++++++ 45 files changed, 1676 insertions(+), 528 deletions(-) create mode 100644 code/__DEFINES/strippable.dm create mode 100644 code/datums/elements/strippable.dm create mode 100644 code/modules/mob/living/carbon/carbon_stripping.dm create mode 100644 code/modules/mob/living/carbon/human/human_stripping.dm create mode 100644 icons/UI_Icons/inventory/back.png create mode 100644 icons/UI_Icons/inventory/belt.png create mode 100644 icons/UI_Icons/inventory/collar.png create mode 100644 icons/UI_Icons/inventory/ears.png create mode 100644 icons/UI_Icons/inventory/glasses.png create mode 100644 icons/UI_Icons/inventory/gloves.png create mode 100644 icons/UI_Icons/inventory/hand_l.png create mode 100644 icons/UI_Icons/inventory/hand_r.png create mode 100644 icons/UI_Icons/inventory/head.png create mode 100644 icons/UI_Icons/inventory/id.png create mode 100644 icons/UI_Icons/inventory/mask.png create mode 100644 icons/UI_Icons/inventory/neck.png create mode 100644 icons/UI_Icons/inventory/pocket.png create mode 100644 icons/UI_Icons/inventory/shoes.png create mode 100644 icons/UI_Icons/inventory/suit.png create mode 100644 icons/UI_Icons/inventory/suit_storage.png create mode 100644 icons/UI_Icons/inventory/uniform.png create mode 100644 tgui/packages/common/collections.spec.ts rename tgui/packages/common/{collections.js => collections.ts} (91%) rename tgui/packages/common/{math.js => math.ts} (98%) create mode 100644 tgui/packages/tgfont/icons/air-tank-slash.svg create mode 100644 tgui/packages/tgfont/icons/air-tank.svg rename tgui/packages/tgfont/icons/{nanotrasen_logo.svg => nanotrasen-logo.svg} (100%) rename tgui/packages/tgfont/icons/{syndicate_logo.svg => syndicate-logo.svg} (100%) create mode 100644 tgui/packages/tgui/interfaces/StripMenu.tsx 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 += "" - for(var/i in 1 to held_items.len) - var/obj/item/I = get_item_for_held_index(i) - dat += "" - dat += "" - dat += "" - - if(handcuffed) - dat += "" - if(legcuffed) - dat += "" - - dat += {"
[get_held_index_name(i)]:[(I && !(I.item_flags & ABSTRACT)) ? I : "Empty"]
 
Empty Pouches
Handcuffed: Remove
Legcuffed: Remove
- 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 += "" - for(var/i in 1 to held_items.len) - var/obj/item/I = get_item_for_held_index(i) - dat += "" - dat += "" - - dat += "" - - dat += "" - - if(obscured & ITEM_SLOT_MASK) - dat += "" - else - dat += "" - - dat += "" - - if(handcuffed) - dat += "" - if(legcuffed) - dat += "" - - dat += {"
[get_held_index_name(i)]:[(I && !(I.item_flags & ABSTRACT)) ? I : "Empty"]
 
Back:[(back && !(back.item_flags & ABSTRACT)) ? back : "Empty"]" - if(has_breathable_mask && istype(back, /obj/item/tank)) - dat += " [internal ? "Disable Internals" : "Set Internals"]" - - dat += "
 
Head:[(head && !(head.item_flags & ABSTRACT)) ? head : "Empty"]
Mask:Obscured
Mask:[(wear_mask && !(wear_mask.item_flags & ABSTRACT)) ? wear_mask : "Empty"]
Neck:[(wear_neck && !(wear_neck.item_flags & ABSTRACT)) ? wear_neck : "Empty"]
Handcuffed: Remove
Legcuffed: Remove
- 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 += "" - for(var/i in 1 to held_items.len) - var/obj/item/I = get_item_for_held_index(i) - dat += "" - dat += "" - - dat += "" - - dat += "" - - if(obscured & ITEM_SLOT_MASK) - dat += "" - else - dat += "" - - if(obscured & ITEM_SLOT_NECK) - dat += "" - else - dat += "" - - if(obscured & ITEM_SLOT_EYES) - dat += "" - else - dat += "" - - if(obscured & ITEM_SLOT_EARS) - dat += "" - else - dat += "" - - dat += "" - - dat += "" - if(wear_suit) - if(obscured & ITEM_SLOT_SUITSTORE) - dat += "" - else - dat += "" - else - dat += "" - - if(obscured & ITEM_SLOT_FEET) - dat += "" - else - dat += "" - if(obscured & ITEM_SLOT_GLOVES) - dat += "" - else - dat += "" - - if(obscured & ITEM_SLOT_ICLOTHING) - dat += "" - else - dat += "" - - var/obj/item/bodypart/O = get_bodypart(BODY_ZONE_CHEST) - if((w_uniform == null && !(dna?.species.nojumpsuit) && (!O || O.status != BODYPART_ROBOTIC)) || (obscured & ITEM_SLOT_ICLOTHING)) - dat += "" - dat += "" - dat += "" - else - dat += "" - dat += "" - dat += "" - - if(handcuffed) - dat += "" - if(legcuffed) - dat += "" - - dat += {"
[get_held_index_name(i)]:[(I && !(I.item_flags & ABSTRACT)) ? I : "Empty"]
 
Back:[(back && !(back.item_flags & ABSTRACT)) ? back : "Empty"]" - if(has_breathable_mask && istype(back, /obj/item/tank)) - dat += " [internal ? "Disable Internals" : "Set Internals"]" - - dat += "
 
Head:[(head && !(head.item_flags & ABSTRACT)) ? head : "Empty"]
Mask:Obscured
Mask:[(wear_mask && !(wear_mask.item_flags & ABSTRACT)) ? wear_mask : "Empty"]
Neck:Obscured
Neck:[(wear_neck && !(wear_neck.item_flags & ABSTRACT)) ? wear_neck : "Empty"]
Eyes:Obscured
Eyes:[(glasses && !(glasses.item_flags & ABSTRACT)) ? glasses : "Empty"]
Ears:Obscured
Ears:[(ears && !(ears.item_flags & ABSTRACT)) ? ears : "Empty"]
 
Exosuit:[(wear_suit && !(wear_suit.item_flags & ABSTRACT)) ? wear_suit : "Empty"]
 ↳Suit Storage:
 ↳Suit Storage:[(s_store && !(s_store.item_flags & ABSTRACT)) ? s_store : "Empty"]" - if(has_breathable_mask && istype(s_store, /obj/item/tank)) - dat += " [internal ? "Disable Internals" : "Set Internals"]" - dat += "
 ↳Suit Storage:
Shoes:Obscured
Shoes:[(shoes && !(shoes.item_flags & ABSTRACT)) ? shoes : "Empty"]" - if(shoes && shoes.can_be_tied && shoes.tied != SHOES_KNOTTED) - dat += " [shoes.tied ? "Untie shoes" : "Knot shoes"]" - - dat += "
Gloves:Obscured
Gloves:[(gloves && !(gloves.item_flags & ABSTRACT)) ? gloves : "Empty"]
Uniform:Obscured
Uniform:[(w_uniform && !(w_uniform.item_flags & ABSTRACT)) ? w_uniform : "Empty"]" - if(w_uniform) - var/obj/item/clothing/under/U = w_uniform - if (U.can_adjust) - dat += " Adjust" - dat += "
 ↳Pockets:
 ↳ID:
 ↳Belt:
 ↳Belt:[(belt && !(belt.item_flags & ABSTRACT)) ? belt : "Empty"]" - if(has_breathable_mask && istype(belt, /obj/item/tank)) - dat += " [internal ? "Disable Internals" : "Set Internals"]" - dat += "
 ↳Pockets:[(l_store && !(l_store.item_flags & ABSTRACT)) ? "Left (Full)" : "Left (Empty)"]" - dat += " [(r_store && !(r_store.item_flags & ABSTRACT)) ? "Right (Full)" : "Right (Empty)"]
 ↳ID:[(wear_id && !(wear_id.item_flags & ABSTRACT)) ? wear_id : "Empty"]
Handcuffed: Remove
Legcuffed: Remove
- 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 += "" - dat += "" - dat += "" - dat += "" - dat += "" - dat += {"
Head:[inventory_head]" : "add_inv=head'>Empty"]
Back:[inventory_back]" : "add_inv=back'>Empty"]
Collar:[pcollar]" : "add_inv=collar'>Empty"]
ID Card:[access_card]" : "add_inv=card'>Empty"]
- 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 += "" - dat += "" - dat += {"
Headset:[ears]" : "add_inv=ears'>Empty"]
- 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 0000000000000000000000000000000000000000..736b9d64bf997fe7d6874db93e079c83b187ebdc GIT binary patch literal 1796 zcmbVNe{2&~9KSMfimXGNBy7{n%z2oji zyN=<)U^xB|L~scZ!em4UlVC(8BLWKO3?fl7s!q+Kk)RO4I2~h4@Lkt7peT{c-Mx43 zectE$`+MD*}V%TP&qCODdLpJQjiBlNLb2e;cv72;DcF@FE zMrBYNt@5(bX4cAKOBSPxA~YlrfXbi|AsABVhz*PL(r~UV6BrtYsLeL4OcRLKyFI8~ zlmTiskT^?{6k1|67|lgR7E2*YkrYXgC4`a0DT*e|bg>CdTo{xlbAGzUu{0qIe%Y`l zRh4Lh2#3Rlu*o3G0m5jtS_zUOC<=!NTxkudOau=pc}WHbP*|ClR9+0B8YANqTT~kc zHJvg+kW#cECE+IMFe1W8gwa51CdGjqo5D#gaxgBOV+jxh0tl%J#2QmrsYz5trAho3 z>eTi<0-$T%?v#xC>LLg!5sK57>{9OltgHeGEgHbc$qzN}#y<{R;XWT*f?J9~5oakjvSx}(o_C0Bin1exGN^Js;s~EZY#6J>A8=S=nSW>xOwop$03`j7yqJP1qF)U&GAIi`Z#^*Y z2&g=v73`E9#NSPf@7-10NjU5n+BDUw;76+2fe`F)GHhjM?&QM`soUXllzHpV4E0=l zv9omcruNU@>s~RKbIg6=z>ad_*sG45!396j+&-NzfRv>o)f7p4hsfxBK`rA3&<_meuKVpDuml?VK>* z8+)^Bj{mpSoA=A(>*ubTw)ClvBP&+V7@X9dR&ZonbNT#U-PH-DKW)$t27XWFeebTcbvF1+{EzN z#W%h0?;BrMm-c(ZP=;{o$yYKw19_9t-36D-=GXcwZj9d9qkn5jW?R;$J*!(swEvXL LS>-rV-nii}bGmM* literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/belt.png b/icons/UI_Icons/inventory/belt.png new file mode 100644 index 0000000000000000000000000000000000000000..1be89d450a8f4d38ccf9a37072687406042faa4b GIT binary patch literal 1596 zcmbVMU1%Id9KRgbP}4T}P%T)f)2+0Aba!X>lDq8CYfSI1cURpQ(t{+THgmgkcS~+} zw!53|-BGA%sZvDr!GNL#E#iYIRuPHfgTX3=RxuB1C4zz$9}2}h2$9yJv-d@+)nb>O z-TC;<@BjTjF*2Ol&~r~uC=}X|%ce%z9|U7ZA4M891+3Eo5vr!m(4o+-!K#h5DdK?wDH&FR`}N~bIAG`rZmeI0s+}Zd zBU^XK{`zoUt50c)&JFGcyQ+v0n8d@NYE~>4RTEr;7qNM;Epng%@um{oP#_SDt0N#u z9Rgwkl+6*b;YK~!h2DL!IhzoSuOK?onrU|Cq zp|#wWo0!AIDz-&QfPqO3pssar_Ow%J2-h`{RES9|&t+JtgSE@lqi&i03w7uC1_8{q zs@jopQ(a86Bf|C4hnN{HhujqH=4&<)M~O?P9gU<9F*UaZ*4QZN5bRMWPidtcsFAkH zK$Zmv?jJKOo%-$rt1gff_DF(bQImKW1Yr}ZQr#%l{sCnHX0H^x z0m@QF#~!{VSl3XIIwocY8zwFh(Y8t)=(rIjX@xpWFw-u&dOw#;jySYvRM>$#n%M(# z>10e&VliG8q=vexigK3gVM`;qRDxs95e!2|FdiiZy-(w%Sd8$R+*jbSrWN@30Erf* zf}|@^+`gZp+H{Zt?fd%wy#JVEuoA?T+vW)hGsqp3HCz_eTB~&SlY^~U#Q=?TL0AjQ zAi-%t{E5!BwvF5NK$F%l6Gr+UdC`QqwCMTRAwwnRty|`u$dng@f^FI%{@ujJ*{b4h z!&%1&rkz&B7M*5CEY{&1*2*HuxffU){34ec%8#GDGW&hcr*Qo<*G^r2>-Y6rE`Qnm zLv82LGnXDYz7$#d{G*?WB9b=UTY2%1!xyr1r$2ab$G%AF;o!ZxQN5UX*&IH$u;sqo z!n6F!%DMT;KR;ZveJ(k5xOD2fr(Rzi3m^Msc4lj}>%2X&d;a>|#^V>aFI>5Bvg=a% z>2LPp@Ee1p*EWY=n?HEu&gHM3yV||>%vt}g_kTWcQl44-(GO2V3NI}^|KzK0^2^sH z^zFM7na9(a^UC7s$(0kY9GLvtpLyaO2f|0TE=P9su3NY1y?t*7{XLf+PQ9~d^5|b_ CA`k5V literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/collar.png b/icons/UI_Icons/inventory/collar.png new file mode 100644 index 0000000000000000000000000000000000000000..71803b1b6c6b535c4a0d706240fdd4da64227962 GIT binary patch literal 2090 zcmbVNYiQk69FH3t+q#WCzH|d6q(eHkb93^(iM{LE-f1h|u5`s!aj=td(wMnMxhYZbeJ-yWs;R%xPHYs{RPWb)8=K}9JIpLsIfQ4{?l)S;& zh#Z(59>cQ}*mQ*6J$z5qVgx=(5MTAnL2OlXLXFpA>-4oK@HI#>krVn-hx~Y9gdd<0 z;q?pz7($sh%#5U~YBsx@m!S+rXowO7vTQ-!(iFaN2+UjLxYlU?(S|SfloLux5?Z2I zsZ=r*B}1d4D4C`yLRpk$zz`sw2@+HVLEP13$dedHUYK|^;8R9q)2SpUFs1bn{IEqE z#Emqugo#xYic$uqA=Q8mZsEeIs9baIV3CxGPl6<7SgC~#OEjTzi7rFk`h152mfAw0 z$H1iH^r+LL!cmm`+8Q?3-pXyV7VvYak*hq0tznPr&#`P- zCRtb2fI~D4P*!CZL=ccQ*+s~Wo-(9E&Zt2Se3m!O?t_8A#|YwbYiH$HdV%KWL+RBs;+37Y3MTP z5!2CNM)j#{vsSQ9Y-SJKtZoCFs5US)HfULe5JQn|?CM&Zb&aPkL%h3QCXZEV&`b5t zH2fYQlPv{&t-~#Z)5*;VIBi+t2#wd?64kx0JT~krCBjJW(#m!3F?Ev)ib!9Pb;3PI zMO;=q4W>!dCO-BWab z15l5B$rQDZ@c&6sBmKC9f+AttjVPqS)Vii#eDS}1QTx{R6K}%V&X6u!TM~O{ZE7T7 z)DhdVw(MGQhHZCOhw^=6yeGe;QIX0YyO)9-@bEw<78Lx#<}rTyFM!ZvH9iK z?d@le8Mn}m%iWd7AHH~f#V3pVZ|r{P`v(`!ym)Hm=}U{-_!F<9^H)k2x$>1SxEp;V zA8uaM*3ZpvS=8ibj+c5?o+j6Ba^aWn8J7>u&ENDU&s{kE{?Vs@y128G|Mc+MBTxSP zcL#Uk!q&aLA9Kg)^V?pz`gLy22X7bVPHvjJ+W*6fPT1SMi9d4c(BN;Yy8klPL1)*( yFMEG{u5)e&n0+m?fVjo4PAz`(k224_`&q|Xqw?Le{FQV!AKEvZKi_}&g?|BkKCR0D literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/ears.png b/icons/UI_Icons/inventory/ears.png new file mode 100644 index 0000000000000000000000000000000000000000..e9a8f3c23c4bf20a2269121cef3797d04f616ea4 GIT binary patch literal 1688 zcmbVNZEVzJ9PcUU#vJM(f=**8L588Pch|eC6t-RG?vmZSjgxJJ59`|JcHORhy0+Wh zEg@`EjKn3z@L`KAX2v)IALdI)kT739D8wicC5VX<2bc&L!xw{LbLi9C-DN0Bq)Gd{ z{hr_d{oj4@=ElkukFCHktTGyrTG5ERZ`pn5YYz_ohK6!8(qUs*#VYspV6RWC#jxd% zXo+^GJ=P>BP^V-Sb^$7@n+T0zTef9QS?L81-UYffLnN*p|CqovRV3Q{F(zh)K}w4Z zT44KNb3z&HRYEGUZ7aSdD~};0X}V~F zZkA{byWl3|Fgh!nG)pnANqL~ElyGLhmCg%S6&j?04h+XeShj>UQ_z8S3jPaq>G&1_ z$hEOpNycq;(e;uD+i4g;W)vK9TeO|XnSgEuHte?)&@h11T<=@RA#aFib-W2}as^m+nWy;kX5pS{fbL zt&R0~v?0v1A)Y5Wip{GViwRM~c4R{VQAs3_b0|$y1%I+j<$}OZ@@kSN1FA1b23em< zvT}e)@;-)Vyh`!D1eJa_1&a68|9Ss8OG70nr|+7_Elf9egotLNsOAc#vmNvmW@!!2 zr;8vfZW)M#;>I7SL}6RIYY!~aW>Nr={zqOc!fcpyGO`8gx{g6By>HiAr^e_PyuFVk<|OhxQ&DS+lxj z=7l}g(%8)xBhCeKX4zQJyKjviQiY~cHsN6W#$z+O=W$*FS|2#i*{JkHsA7rWS2z$MH v_WAn{i(d{$uIGBO)xU2{Z*4o*iM{d5mDXP$-o54{_umn1XqKkyJ4gNk667+U literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/glasses.png b/icons/UI_Icons/inventory/glasses.png new file mode 100644 index 0000000000000000000000000000000000000000..6e6f1ad098f6d52ccdd7001ae68b001ad0e497c9 GIT binary patch literal 2017 zcmb_dZD<@t7~Y7fHZc@z3&kIH-6(0r-JRXrZ*sQjT}%$W7?W#r{)l#VclK`0?MHSu zxyxZO7AuPSLlmWL8c-A}QjJLQM=1td3kpRLwD|pjC@3acmDYdGUha;<0xr(l(RSh?oUgKZS~R ze>FhE)q$K*9Wzvu+IA<|UD2=s2St#qI3+jKDoLtlSHtbNo2AGaBpORnJ+VVFpB*Gq zUVuo60ki=CPs%F8NrIp#caS{b0Sjc712oTTK+?nnS-&XkEif%@D1BGm7ye69#VGPM zmMxddOgX`LL4oB|Rb>Is@;r?ZbU5WkutK|GXTw4og+^fek?px;Y!T|-M3kg3)1?p` zziHMD>uJIXV=K^SIR?Zb)qtka#Q76Jspi}?SX4p|a-$GqxhB>xdXX0vy;V#%hyUdO zr#72y`dE>R<1}4_QST%Uqn?lz(P3`NN9+&^y@|j;y_1+`XB>^Mr2+&aFUWabsZprG z24#}x89;6sv0c+Ehg+9TKxr7EB!#QS(I7!{Y7U4Rr)mj>2C@dg5;W_Xwl#GT6c?Bi zH383D2E~#wVFWJ=HVw`40te!NZ3h+*>$?StY^G65c_lBvjxoE$^8QRJH5ho7UBUz5 zP~Q$R)0>hwRg!3);c9fVSuNv+5p)fdNhc|s9LBay4IvIDWI?BeL_()k9rIQ!L8WED zLzxpe(-efp{Iq9G#1d%CH}QN`=M`R2O|!pTchM@W)+QB`P37CAa03No#m7NlhZCPYOxfu!h- z&@ZAMZ$v;7g@#VAhI&1%MiIINg!fdI`Y*v;i4s?-!-b3P{|knVx6&nru@{Q3Hnwcr z#f7YO;k`U=HV+Z}(>#Wdi}MuVLuQ@&(EIp+I+IEFy^_53)m=Mxe#*X;pKgus`Av_KTW^`! z{LPlzPCpKg{IT_u*$p4flFpg2*57n<=BtI#^W!ZK%-jtYUhnMx@sVo}zqB|{s?%@I z=et0DrsemOH-A~|8h@Eq-hSrfthM`vX}49W-hS%*!HrMv8rgop=v#Z^)bWK=8@lgX z9R1;l_2IeEuaExp-Pw=#ztc4a`iSw~f9V|`JpB2I2f6k+qJ3`q*=yEq-n*yA TGWHM0hj6BMApORU-FyE5goTfO literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/gloves.png b/icons/UI_Icons/inventory/gloves.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8a16cbdb7afb31597ca4d51d3fd42a97b80d74 GIT binary patch literal 2107 zcmb_d3uxSA98c|R_o^+5vvh)4hT!Ise97gKOYHTrySkq9YUz%yL#oRq-`$1XC0Ua8 z?m8!>>|qhg7&4hC)+gJ9c4Z*UDUQM_)m1i;DN|vJ&RT{WGMQE?e!1Qq-J{zm29kV_ z-}is~{*SM}wPi!a(we0hhE;@`gKg-KyYJ#M^tF3>uA<)(vw4e+VapzH-xBP#*Q+tC z?5Gy$a5}=KHoZ=1uUR}Kr?`3Sqbox*MsLD0%^03_D@WBf7YK zodQH{I9$|mOIuQ@q6*t->PBwl3ArWOj$}}bF8a7ldmc0pz z0?hCd5Uzuwlu>0zz9U#wq?lo)WaO}xlH-sz^*DhSX_Nv+(y)+XWSwX3dMFTRwTzgS zL>qS7hV^);DZn!#&yy_0Z3pxP|HFj?}E#D5}|f>1=}A^P{AO=h8)z6}JrhgyO~@sziQSy9EPx=fSMl zbOIuwx59)vXt$NlW*B{A z^5ypCv|SX)+!E^L?yz`?ph@u>f;!^KLYK^mYZtCzSP2sfHby#j|L|(~zCKX3`}msr z{=E$=PL7g&D~Er6ujX{~p08yoH8M5%)1&WlJD-<^1LGI7Ump5nI@xe)JNMV=iH)ne zuY7=Q?eTRQ5%6z22F!!+e6F*DuK)=f9NRxqdGSGf@-Tsp8 z0>QpjieIgM>d5qX>q}MpCK^I3_Aftt*x&x=L#1nL{ywyJ(d6XYU#uUj-ghRMEII${ z!zTxNs|N0|-ls5Xu2&GflmG(=Lwd2F*YWLjVJ3KSJYbJPU*A#YcIrrn((B*RH>n6_(Xr%h0)*s$)0-i`+HwFQ*M^8zO zde>8QOq}Rnr4)w6KQ=@+6zULVOefMQisBy%vmopbhXa{EvHnCY`f|NHx~<(tF_65M z_deg>zn&W!?BCRWTRX!rn^FVGVY;%xyD>z+UbXrwU9NKm9`YEbkocrmW_c+!v;^oO2tee*t2%xS_ zr<*cX)umW$itzlt32H{&A*-Uj%%p?GVeFBTYv8^Ks^*r!8V4p^jC|r|2q`oIHPld< zm1P02_w2E3la#$}D+q8B`8dwes7X9fcuC8E2$VD!(RdJr04zh(#I&-Lmq2NNB^4^* z0w_%x6Zzsn*M~22nOkLuYXc+KFHxVC=AppWM zHX}f7&?kve3R0j!Z&LcG9t*2kjQA`FtGuaDdQCM=UXE$1rXvtjvEHEH=~}b|QDNPD zg2D`P2M$;sjp}5*bavs#>bru)*3tzcBPfG7X9V%bCRabUR`I~qS+LBuoX3>uW%6Pf z=8>#lMlSBjQEy!_@5Hr~_t3=URX0Gy0E&_26{OMNDPfb>qtOT-q2I7>$P$jq4bv}C zJ!nJ<1ac!!*Q0vftwtW%IZS)1$gSU)tINbS;&5@%{eQ-YK`UL(F!AE#tKF?=yEKrs zBihS@ZF7jwpXM0CHuaNBhs?}}U9EILeVI!3WJccq;oyUtPqEh@IAP6ykT@;8+Hrj0 z?Vlfbcx?X^7=3p0VLmkd#-Gv8wmtGf?wN(rb2CR7|FG3|Y1}zl<%#WQ z@6^vax6Y5*o!uvIXJ+Kk^U~j+C$E7XP)}_nxhMQ-~E2a>p*Xt{)d^~4G4Dv4;-fH5FVcxXYy0MQp{Oz`83A@Ja%G4a8Kgpm58J{T~8s1Jy<_mx&#AdQ#o z?d;6&|NZ;_=g`n#|EAU_S}BU!lpIK;$(s(IjVI$L%L)YY)>u4n2KJNtb=Wip(_x7BF6Lt2I;ZX z5FN)ZqU8`^H2?%UqJ(%k9F9hxr3D}W4n#N}u!5ihS(QY(_G5@OS2xsjVo%K$`HC@l z-*;4wD;A5Pq8P$%j^hZO3>l>5C}r50ClZ_b0*zF#kj64b_d~ z6$VIZQ>liHReiCnh6&H_n<8P<60$1V%S=0nOCt|Yx*F=6B5rOEqH)x?i=dC)4910e zp@!-%(}EBJ^o~)})^XA6SSElH&_^+bRE=kW$nr`CNGh+WQiKH&6@V%}=I{tuKC zm?s66RzL}5bm+tTf^|(buxmjQuxY^@;v752(2X>zaa_PIF-*J@m+vRz@gWx*W`P`d z>Hc0i*%z02MV471#8=!+rPQSD`Owx-G7)1)azdu5s|rMdD2g(Rpa|Hk9*(e4B@$*4 zFF|M^2sB-(%O|il8Bm}uuM>Gm07@2$3aeygk!?=HwuX46* z?2KcE){K5X@;j0UCM^8|**~|U2SBu?;yeExq;JuqyzSQ@1lz+c$J+JG%Ylz)6K7rZ!TOs Sb@s0yL@L=gm^j}%{^mc4t&ljBz1!V& z?WT)!`~izG;`|i`1|pj9kA^s1LWU?Brjls!|os4x!GSXZ?K`H1eY6eCV#Kz4<1IT^QCQ~q@>H+HF^v4vbDgkPTH^#<{ zAk3Q2Z*eX^)fn>UdgixMW#pbf~PmeVb%7@$hL5*|C#3`Le8c3*&McMOur zSPvOQCL{$HOUo?FlRnYK2_BE%-$wE*&oZo!;aHmIC03BUZnAt)*qW)NrFf{LYzuz{ zsH|-p62lY<1y{lCLS}~HL{Vf|p5b{KBWP<-w?UECt>y|t2wJkK8n%jb(qRNCl(z#E zR=VJVW>jf)s~jdAFs29$hI6rwOC_KpS8+z(%$1BQG6Qo^gSu^DELX)ES!5$Ci6l29d8UNXhL8kGl5X9 zlBk}FWRmAyEZMR{)fH5*S{H4AAz;G*g|o)dtefV<1nZSJQS!KH)+e#-0yKscH9h!0 zC{8fv!{}?ExMUPygPVdCSxO^Q1K43z0~yE|dWIsaVU&U>hfHi3t8*`&j|PK1CQ7S0 zykNyUw~*0rP~b#Cpm`Tpk{gRjQQfkEF2iUjK;huHR8^5YP;q-bUXJz)P@z*k4^M+s zilqe}C{U2yZm7V@{t%M$P6<@@D|kOhi@Y}tA?@*skQRL&K>JfZmUeRjulUo7->dj4 z`?s1ZK7t^3+dR&}bZSS6s20xZVEO25hyCSIP9;m_A_3Vs1_4TT@(&fNJgqJffm`!n zfwqu^nDhp9u>iAB+AaVSwr6m(Zd!NDa^fv8aAWDWmb_)8Utg!UBfT6}IScd*#66Xv z9A_A(`z$1dxjtW|wMCt(0>|yo8CLr){-}0tsAFmq_hovYIY$sRHPKLeBKg^$ug}+y zvW+9dU!8gE#a)NblC6&%>za)3-u2}hO>;XZ&vvP6;2$q-{dQpb>fga_duNkZ9;&_n zr`q1b{k3mRJ-BnYv)|e~5!^%(V>K<(2VZZyukn?c`8^{?H+&dARAc>k&q&=zW1qKu zb7-*c!rhZZZ`-+F{%P(%PE3y|S6aveowbp_hTmy=e{`o>G$ad+p{6MZf^W!NAlmMsgAX$I-fbwapvTT_t!MSXzSXezwJ4`@5qt! zktz1lu@zc=_nYDM>O>BWp1Ab#11sm(|6JQU((vqf Y!~Q}vd?9@Lob!i^hPy-WZFze1KQT;ZSpWb4 literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/id.png b/icons/UI_Icons/inventory/id.png new file mode 100644 index 0000000000000000000000000000000000000000..4469591d36f52d65a994e06e2ce963ad6120374d GIT binary patch literal 1940 zcmb_dOKjXk7y1@p#u{?J>5K z-7FjshlHY>I8}%Pf(u*_aNq(`Ri#KAsT7Wgs$4)w<&hkE3*&v56q2-xuw>78=9~Zd z{qtX0n4cXPerlNGxRLT)X_1Y5_C7qszDc{i$A(kU+~tJh9yy)81Kj%`oZ+~k&;80$ zy5wAdm&VMJ+K*M*9*I+hy)dzW z$t@JgiUcy3IzSiaxM($Qbd3TpzNhY=QOgT_oTvrap>UM?0FV(R$~wvjB(p6AQcMMJR+LsTWD zW3J=Ca+stj#H3uZ1r{91_g$zKbY1g2B5JDXimIYx(KL}Fx~>OQk08S|%>MZj#j6|C@ZBy#v=SOJrw>-R0RdZzAj`Z$l(xaf(@! zd13U&JFG#yTP{simQLZj`}-5ylhuuv9@>J-I|E?mP4d&L?c3LX=B`e@_S@d+y&K?- zFChP^|CsZ~ttNK?#oIgAzPtLwqfh_x*49_n^41;Bokhtr(&p&y=EmTVKK#!0yJxRR zo6^Mi*~#5=++X{*w-@X)jh9<>bNTznza4z#$(uh6d_3~r&6!_68JziMklXn%*!}3s SjX$%#S)QIReRh8N?Y{ws0&39! literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/mask.png b/icons/UI_Icons/inventory/mask.png new file mode 100644 index 0000000000000000000000000000000000000000..82e51089379638169f03a711e11b1a6312d06ba4 GIT binary patch literal 1930 zcmbVN4Qvxt9KSBv0$U&mP6!Kf-i)B(dLR8*ds&BDyN?FDEnCe-LbhG+UfavsyW8Du z{U8R=kp}t=)l@>ju$g zPd)0C6o6WdB+ii}jh5Jrl+|pu*|wlGNs|OwLQo`5(+p{4EG9I4VbGew`MJQv_KPBoWmaS)bIY zIWVYn!G*A#r4`k5m|(z&C@T|`k3}SWR=qJ95Dh?;Iu#C7c0rlNx;HZ8Q~;|< zidT|CnMBoRB%?HKB+<=Hg2+n|_4!2`fQ!|D1A|$kaMFZRb}wmRC_7`a;$#U!k_%9e z#0&n||DZ6zG{soVcR^vv@T|r@6wGssUsA#>bXW+p0YJ!N07J83WSmk+QlMd|&a`;m z?R3^Fl3xhH1+}5N0(Dn9t(4tr#c3mzlI!s>Zc)`(kppg*1B1aa3Ifki6lb9*s|6=1 zK;ytI-7Y~O_++0G3_NUgxWTv|R(Ac(=^ zc9naZ$1lEHSN9sZuIJA~wXg5nAD^YWC$7}L`16kha&0AA&!G3Gm7m16X9GE|djE$TrAB-9Pn(gh~k!;N$S(UUOkN?Y~#53 z&fN83LvncIhl$qrzyGZ}nTr@#4mgjBy?4Gnh9re;eQ)&LuHtuG>N%6>Zt<>?4-OW# z{Bi7quTPE^7OlOu^S$rpkM1e*@1Ja``lw+3r>WVMgA*mY{(XDqhW@v7SJt`)E81TD E4>iM=I{*Lx literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/neck.png b/icons/UI_Icons/inventory/neck.png new file mode 100644 index 0000000000000000000000000000000000000000..78ad3ce3b1c7d608523bea7f3b8ea62fcc789924 GIT binary patch literal 1852 zcmbVNeQ4Zd7|+I5+Rmv^=Imf2W*=^>N#5jgxhC87V|TaqWY-?;K`RvJd&zruv3Gf6 zlIvZobgfWX{}KfUIzeS_BFwF9D(YY#;&g7v91a|;%*qhPX4xO=A0jf}ThI`1J_O99*^6!j{{6TJYR_C)0MXhM?t|ipl z?>tFS%dZ=Wez!liOVx0WgE~$ku9&k3nxeM17A>d^B9~60v|$FB+b2#jw4n!?K7S0v ztPsi=og+5t9qCGFBZHcvGp#N3_M%D%a>#{rF_$$RwHRbdyee6HuX% z`(trBgl$C29AGs7L^_~wf-FhR%}uljM8JapF922)RUoT=A6@=2#G9?B)SmFpvM=%! zWHPR6sXSjO6u5$q!*-e%6h+~I$crLN5Uew7y0FNa&iV>N7&)44SgwIh+GB)CoOgo^ zQ92hw&Z^RyPB~2^VSEuT8SG*wgBPK$ zZvV#tNo_1v^|7FqT(0WEaU(+{jB-L2L_3LL3-LY3!FgLlks%^;y%&w8hHL~~Y$q_z zRtgobNTx-R1N7!T!_;xX*)q=ng`tar45^yH0v{_V3E)>npprERr~u4CV^}v*!~cVl z0t*3EB9`-@q-AvI!ux`CO-*4t2T8z24yF-rnQ4Zurcn*yEVhYbqRuz}eKZt`+c;%p z$%fO@v4f6ALb9O9GAnXINp38rMoq_sriP;7AVZSF8HTRvq9mmJffOssP-caI1X!pi zlWf2bL|sb;BtL)^`!Lq>UJF$0bz%=SO;>z^!pc%oVxb~6v(1u3=oB3yKk`FCNLB2+ zZG#*^n0;s-?_hepqjnk&sp@d~==7o&%d4zGm)eEM@{U1}(Y*R2ohiRI7RbPZMKDKO z$RI*`kG`0LIXLAOppDwoBwP3GJAN_o4jj6-^am^6BGT{f(}$5>POFxIW*U*1$}?Ut zyf=O3io)MruhQPUNmYT9@#ih8bC*0+CpR()wN2*o#e=_pM`rq+Xt+JmKQX&6`si2S zk;qHEH+oameq8wa zui%ogzN;7Byxu@geB{`QE2sC2Czo=?t7~i9_q1*LqV0t8#ybD88{MD$b}@C~7w6}v z=-u0Drj~X5G;n^y*73$a51c$Qk-j!N>6}V^7Tq70*KPQKn%R5uj&HfXHgqk&d*^um z`{VWJF1;|duekcTqo*4ZY?E!A9XTdMh@B0>wbcK)Xc>a}t0rPNTB>(^b literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/pocket.png b/icons/UI_Icons/inventory/pocket.png new file mode 100644 index 0000000000000000000000000000000000000000..f42399dca0f57aee93ea1c8890690e1cb09eb643 GIT binary patch literal 1831 zcmbVNe`wrP98YWKX-9`+!ba!DBdxH3xxA!F?h<-#-Q9I};&ol`pyxzvE_rtsddW+Z zw0A8-y5e9(|58zKLzrX!rJaJ(O$Ceo;U9MYh-2;kbIL$jw<=bOO!Vb?chuQ#V}T?u z?|t6q`}zKO-%R)S9d2uFZ>1=zEt!Z7kT>w}#~vk5cVglX@_O7(yx>yQ)*b%cM7?`v z7e#IP$;f2AZ0dPM!)8#`aSjD5rcKZk)qS91tJ*N~Kn@iQD?;D?;wu^$dW0SnQ!r&m zQPD_DI4C{Qm(eDMHCd+*JPW!j3L!9&r-F)EvRtJSp{u+K8T->L4XO}tI70XM20=E} z52DyXKng-egOCRyImk(Z5Dq^Bc*sK*hFA_VJg-1W5xYR`qKP#}&npA5-kL4)6`_ls zXDcjQE|-Jlt{`>_EGNq{3wf638G>Nkam!OHjOBLJ8DhxQ9K-euYyqE9&EZimLKCHH zE|_+M)^clMA^~G7s?Bmi=(|(}>RJP5k2=yAR)Q#o; z7$Bidr5ZLi)MA4Y$fU-)x%B(<5E3Q z{dLKJ=YtUJ8#F8(m)-sAHlUd5p$JW~#xbyq;p7Yy6;4(JnSmh%!Zm0L>qdV3e^8QO zF07DDuZNP7(N#~qFId-B}RpE?y@+{nuoyCzCz7^mfm-DfmSE z(gzo#7bmZ`!?T}XiS5QepPx_c4)h*-?tJIc`>)NOUAnVVi6$uU+w|4tePGMjl|3!v zm!JOS#7gPUk91?C`^a!^+l{|=&IRJ%y*zc(OE2tY!T!Mazu$S}rEmVurUN&6Ps{}N z0%+bc-Wxg@XXaPf$vrQAe5-xe?2eHiPoFz}>dURKpFUbX^@eyg6rcU}hvP>NZeJa4 ze`@Z`a?4vr)}<4d7yoIx-MrBGX4@s|SXb+O>(Dzh^7c>jtIgM@?VGQpdl#B#2Dz1M q=O#a!%XVH^Js6q#aIq!OvE1}zYpnU}3!ixYuPhnwi_IJwn)(+_K2}r! literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/shoes.png b/icons/UI_Icons/inventory/shoes.png new file mode 100644 index 0000000000000000000000000000000000000000..d20f7ef4d10615f467b90ca65fbaaf85b49fd4ec GIT binary patch literal 1817 zcmbVNZEVzJ96yjPz}#47G`=LHD-lp^`?Po0yEfd|xI4E?=(vn?oQ^Ph?bExuYoF3~ zyW44^A!;NVrzVa?x5Pjq#18}w(I8QL`Q``02SsDXhdGplM3g9M2);bMmuBJygr>bd zeV*U*`~Utg&;RzP`+Hkg-@iH#2(+g95(D^6c+bja{OeRIzv0&^tM73q5NNr}dzJ;> zoW3^@XqwkESvMP`Ut@R4lp3Xuz=z?5%6L-NYpP9TeDS797sG`w}roA zWWjYUkzq=u5?u=Vbf^x=9S1wbgv#!CAfR1A8ma8L^@EGMB8g*kN zR$6nxuzXt6sfP&%j48_&!_vTWsR~pTA7_o)#j0^tVPFv&&~zP)WqqtwKrV6$XbEb6 z`9B75XeG(Fv8Wcq@J%>watyms56GfuCo^tAW&k>9)K*|}49i^Sd1Hxj8_F)SGYAzM ziApyl6C6haV&hKTR8h%!Xu$@UkX;xfan@K01SwX?fUw94VmL&BhzLLpDj`+Z#{UP! z31*`r&eB3CE*VvJ<(q<4MbwaO$k<`skn@nS%sff>VHD%2h-_>as|zliPsQVD8)~QBM73N*S@DiRj8we*LzS#g>x)F-);y@u zmI@G)-k>gOFb8RFNw#569!Kk@b;m3v-jT;{EdAD!w}kZT>-2V{*Tbq5WHS$OPi08Y z8OG~AwWKiD=c~H5pi?#AxczxUzwhD?zjH$qQ`@*N@9vmCk9+!TD$$e4e(}>AQp>*X z_80zo?Vg!y8$U`k&3t+M*ukO6gC7S z*>i>Cr#8Jim%Zcb3$qj9(>?1ie6wQqT4&*E$J%e#|B^cL!KMfIUuv3Wnh#t)^z_i7 z+>1y5ZtiLueSX`Dsn5Re-aYkj`mIlfcbywdc8xTh{#0{rZW!=brHXyi&>j#L=yL_WcWGY*4lU literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/suit.png b/icons/UI_Icons/inventory/suit.png new file mode 100644 index 0000000000000000000000000000000000000000..e9c48e8069f75f8d3c100e570f4e18b4bd543697 GIT binary patch literal 2073 zcmb_dZEO@p7~a+^LTQ6WB?Q0LZ37zF-Pygn-R|0xws(cQ*sHZYp#1P-Z+7l()4Sbu zciX%6hqi=h{9!_DqKN_09|W`l^20PGn4s1wIbw|(v5CQ8V`2!zl8RztqR!q|3ba5P zFWKAKnP=XQ=Y8g#H@Z5v*Vk^WB?zKE5)O6aH|ajBSK)tqXy___tue#9ZGu>PpZio1 z@9y6~5UWmTv0kS)+95+D!zii|M@%kbVl+WCZ_Sws>_ZM2M@dZ&Q1howQ>3N_s9pXj zh?+r^(!xU)>KW>c!J$4Vsnph|$>yAl4P=m`khx4+x8+=bD%h3r*qvr6vH)@V0#vK( zknD|ikwL>kq`&|g0>G1^#BhSo*VMFyDsiLV z2~arFVhS0vVpg|HG-1NnoMN&Z1KgAfKowSSX1|p#I9DNy(kO#;$HrK$f;CfyW7sKU z8Pk>J>l|QeqtS|wC1c5CDlTlNeE_FXB4kOl9UC+e+l_3a--4)p0EgM&W@E}h3n`9a z#S9}|E>u@JWRm9@Kt8@p(^VsDKe1>73Mmc>P`GLw4ZJib#eiStB$*dzAj$v~p;1HC z5`#BEae*-uh}S`}WK_jbZVOhSoG`46f)my3$Gj_9_d=nzFh0Sc4DXqqZZ67Y#gKs4_Y{4_85 z5_FR!A{uZ|5#n9}055h_sF)lwV81JY@_rTXhtMlZekjs$2>i6y?-Oas$E&m$S9o4k zMb!tz^8RNn4Ie=zy<$7=!F07Fhcz2lb+B}FdeF*%O|y=D~Y?#93iI14j#)N9@w|z?ba{+FE-|K-n!BA&BCK^jUB%<3=cds z{MWgeO~RF(WZrW*|Kow#Lz`RFhkH)FlAdH+cVFnKZk+jg_>AYh^Wfm$#J`>sSEm-P z)y&N;H0D1z{QNV&cg#yw`6nM(J=#D{c|M<*tp2utwr&1oQ~uKF7V&;(r+wt5BH6NXvTtD{ljA#AX?;rflv+%>YacXSGh%o=^npFMB zIC1T%iDScm3|>f9kFNVP{pZLxp4hL$%*^JWQ`JB1zdQWdw!L$2ewY|tJ6reqQAm&c j^UGzXp>8}IHaw3U@oi|xPxlVEmv^MSGjzP|r9J-vG{UPI literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/suit_storage.png b/icons/UI_Icons/inventory/suit_storage.png new file mode 100644 index 0000000000000000000000000000000000000000..9722eb10297ab295a9c9248c58ac4426acad96c4 GIT binary patch literal 1976 zcmbVN4Q$j@9PcKF$Ry73kp)F+(W#mJysus19OG_mm#kxsBbH=jI17MkNcJf#{sd3||_I0#Os4q5>u=MkkJlC<(saZWlnlLX))oLYo<+|Jr%>SX@OdQBYNZ3J!LX_8tL>Y^twr_eaOJDqUqLMfW6RPiea|n zaxi3uggRbS;}nq8Fr-p(4WTh?L1jtMwrY@Wgoc{XMOfW}WX`j^(RiN@fvM_2RgFy~ zs$oJh&T$lj-@9B^BsFQ=cisl*2PX6qC~GXq*h$tCWSl(f;T>+0aq|o_4i!{Mj;c>g?yw2YkK;U5&^w zv|u#WEW!iTJ{Rk8xk!#;vvLK24=9ES6cGmeUIGP&l4XgH0J}3%;S7^Z#8W|v5^^zY z=iFpOWbK?Nh8YJ3CieSPF=3U!#C{3wce+^UVx3Ns16&0uIUJDm0Kk&r3bzCq07N^J zx4%S}(Gdi(>*lczrd2z9t!$vIw&sq`Qn)5JiphAkTzDW_$G}U7R{o(x3+{c*DQlI@`YbY?rMh=y+yiYsbo+cby-b9i86TiX^P6Il1y<2&s;qAdsC zewYrFjP|Fx+E+{&EUmORBnBQ_7%kzn>1#);zFBv8*8a5{<3}bBbYt(99)5Axqn*cg z-M(pd+2N7jmlY1}c&Ga_$FUvLUmG?_x;>J4sc^+{ScMJte_-D{?ZB4iGodfA*ZU6r zG)E6Es>8RRrT#cGEzS5l^1uupDt=|O^-2)B}8=| sI?4@>B@1q@R%>UL?QyPJ+=b1o^uDokjEDRFvi_}s>U#gSMXT5U2WnlQ>i_@% literal 0 HcmV?d00001 diff --git a/icons/UI_Icons/inventory/uniform.png b/icons/UI_Icons/inventory/uniform.png new file mode 100644 index 0000000000000000000000000000000000000000..292b3324b5bd9b0a82a56fb0de737bfcd3e58bcc GIT binary patch literal 1885 zcmbVNZEO=|9KQ`&wl%{s38KU0xP8F4dtKk|ps=moOVN%}8dnIIUGJW@=X&=Xcek}` zGO~@BI46z|OF*5*7-K?W*glZ>!4EFZ511trG$9(IVu&a~nK2E~7}4jtwi&oNLX+z~ zFTdya|NUQ{|BZ)xy6S2luf;H|F4!H2pf_$m)m7-<%;oavRik$wGBM2Yi2YPxZ%%K+ zu#MN0Xv~U*o);vjQlbpwfXb>mLSxwOy;)t9`hkVVK~m9t#L~x~5V#`yh+bES3F&^2 zQo3^nIGF2+O1XZCmx;Z5@ZDJf5vaft@vNHGOd;zd3cLau+sib87a&%@k7%>?rgEUZqW|;`fma%#YTF^|vHK@zm zcNsvT4TZ`!R@I`aWfP{=F^JqK24q#V86DCA9RVgBFeK11h-5a~-spng0HOtrD1_-! zqQWJ~ILA>8{$#JB$uMI+wc-E>h!*e>C~GXqw2&+xWn2Qw3-;6_Fw8PE1Z5>L^dBfn zFvke2^DZbV8CkT%b-}VEB%q;+$YDhllYrK>B!QR1DEMI-8ptqG*Rpaw==X;Wm{8Ja z!;Eyb5(ox-1PTtND6-&XT#_fwu%yT71;|-voaDW3 zne<8`%fwk%+{2X52cR@ymq6*fTsYs-B1s93Bi+~e{}JXf;6 z-%!vM6w?pPV_!_Wc7$%lL|GjwUY&#BSaFnA@Itu=qGVqKA0gTK2QpDyR#uI`{dllU zn@Isg`agBC3^QTE%7_MNOQLA4TX*zY;!SaIJ?Zz8yfviXTc-~qy%<(0C2C24dMZuW z&d_%ESxyRlZ@vn9D>_vPj@q9+EcabBDR*w5A!-Bl<)arr-;ZGxcrefwjm`ghI<#qw zsqf?`lfPjvwn<}WPR(9U5$Vscac#?ypFiC&bhhfb8}CLBFHD9Tw!%?sPg8hTbmCg< zX2Y$T;Y?@E2bqP~*N;}tR4wj2^WEDR|M%vRkso*DjAPAHeZTlCw@DI0L z9OOR-92eLl(-Q}JM~3t7UA&Zyyz*N0Ur%po(A%fxdYT^F{Dvc0nHOqn^*hH;IxZ}Z zUfyUm&U~|Ne!M#5Xr-<+R#j-bsKZ|@e7GU4-dGx4d?@hk?7oLn=b!kf`Rdit#mS}7 z+I;uOrfK;4)K_n*P0!3tHqPI9=fw3{U+Zv1HGK8UnZEk^mwz~PGIDMzAB)ak*>QH~ w+^stC_Hpj$pM2ux$f?FI`2Owd`R^;~OMm=yKJ~)(Y5M~h?C1%cYwsKT7f3W~>i_@% literal 0 HcmV?d00001 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 && ( + + )} + + + ); + })} + + + ))} + + + + ); +};