diff --git a/code/__DEFINES/strippable.dm b/code/__DEFINES/strippable.dm new file mode 100644 index 0000000000..f62c4d6c1b --- /dev/null +++ b/code/__DEFINES/strippable.dm @@ -0,0 +1,30 @@ +// All of these must be matched in StripMenu.js. +#define STRIPPABLE_ITEM_HEAD "head" +#define STRIPPABLE_ITEM_BACK "back" +#define STRIPPABLE_ITEM_MASK "wear_mask" +#define STRIPPABLE_ITEM_EYES "glasses" +#define STRIPPABLE_ITEM_L_EAR "wear_l_ear" +#define STRIPPABLE_ITEM_R_EAR "wear_r_ear" +#define STRIPPABLE_ITEM_JUMPSUIT "w_uniform" +#define STRIPPABLE_ITEM_SUIT "wear_suit" +#define STRIPPABLE_ITEM_GLOVES "gloves" +#define STRIPPABLE_ITEM_FEET "shoes" +#define STRIPPABLE_ITEM_SUIT_STORAGE "j_store" +#define STRIPPABLE_ITEM_ID "id" +#define STRIPPABLE_ITEM_BELT "belt" +#define STRIPPABLE_ITEM_LPOCKET "l_store" +#define STRIPPABLE_ITEM_RPOCKET "r_store" +#define STRIPPABLE_ITEM_LHAND "l_hand" +#define STRIPPABLE_ITEM_RHAND "r_hand" +#define STRIPPABLE_ITEM_HANDCUFFS "handcuffs" +#define STRIPPABLE_ITEM_LEGCUFFS "legcuffs" + + +/// This slot is not obscured. +#define STRIPPABLE_OBSCURING_NONE 0 + +/// This slot is completely obscured, and cannot be accessed. +#define STRIPPABLE_OBSCURING_COMPLETELY 1 + +/// This slot can't be seen, but can be accessed. +#define STRIPPABLE_OBSCURING_HIDDEN 2 diff --git a/code/_macros.dm b/code/_macros.dm index 6c1f37b4bc..0e85827bc0 100644 --- a/code/_macros.dm +++ b/code/_macros.dm @@ -67,6 +67,11 @@ #define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; } ///Accesses an associative list, returns null if nothing is found #define LAZYACCESSASSOC(L, I, K) L ? L[I] ? L[I][K] ? L[I][K] : null : null : null +///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; // Insert an object A into a sorted list using cmp_proc (/code/_helpers/cmp.dm) for comparison. #define ADD_SORTED(list, A, cmp_proc) if(!list.len) {list.Add(A)} else {list.Insert(FindElementIndex(A, list, cmp_proc), A)} diff --git a/code/_onclick/click_hold.dm b/code/_onclick/click_hold.dm index 996f7ed2bf..e972821615 100644 --- a/code/_onclick/click_hold.dm +++ b/code/_onclick/click_hold.dm @@ -97,9 +97,8 @@ /client/MouseDrop(datum/over_object, datum/src_location, over_location, src_control, over_control, params) . = ..() + if(over_object) + SEND_SIGNAL(over_object, COMSIG_ATOM_DROP_ON, src_location, src) if(src_location) SEND_SIGNAL(src_location, COMSIG_ATOM_DROPPED_ON, over_object, src) - - if(over_object) - SEND_SIGNAL(over_object, COMSIG_ATOM_DROP_ON, src_location, src) diff --git a/code/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm index fff5e9200d..4dcc0d6468 100644 --- a/code/_onclick/drag_drop.dm +++ b/code/_onclick/drag_drop.dm @@ -7,6 +7,7 @@ */ /atom/MouseDrop(atom/over) if(!usr || !over) return + if(!Adjacent(usr) || !over.Adjacent(usr)) return // should stop you from dragging through windows spawn(0) diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm new file mode 100644 index 0000000000..e0daaee74a --- /dev/null +++ b/code/datums/elements/strippable.dm @@ -0,0 +1,536 @@ +/// An element for atoms that, when dragged and dropped onto a mob, opens a strip panel. +/datum/element/strippable + element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH + id_arg_index = 2 + + /// An assoc list of keys to /datum/strippable_item + var/list/items + + /// A proc path that returns TRUE/FALSE if we should show the strip panel for this entity. + /// If it does not exist, the strip menu will always show. + /// Will be called with (mob/user). + var/should_strip_proc_path + + /// An existing strip menus + var/list/strip_menus + +/datum/element/strippable/Attach(datum/target, list/items, should_strip_proc_path) + . = ..() + if (!isatom(target)) + return ELEMENT_INCOMPATIBLE + + RegisterSignal(target, COMSIG_ATOM_DROP_ON, PROC_REF(mouse_drop_onto)) + + src.items = items + src.should_strip_proc_path = should_strip_proc_path + +/datum/element/strippable/Detach(datum/source, force) + . = ..() + + UnregisterSignal(source, COMSIG_ATOM_DROP_ON) + + if (!isnull(strip_menus)) + QDEL_NULL(strip_menus[source]) + +/datum/element/strippable/proc/mouse_drop_onto(datum/source, atom/over, mob/user) + SIGNAL_HANDLER + if (user == source) + return + + if (over == source) + return + + var/mob/overmob = over + if (!ishuman(overmob)) + return + + if (!overmob.Adjacent(source)) + return + + if (!overmob.client) + return + + if (overmob.client != user) + return + + if (!isnull(should_strip_proc_path) && !call(source, should_strip_proc_path)(overmob)) + return + + var/datum/strip_menu/strip_menu + + if (isnull(strip_menu)) + strip_menu = new(source, src) + LAZYSET(strip_menus, source, strip_menu) + + INVOKE_ASYNC(strip_menu, PROC_REF(tgui_interact), overmob) + +/// A representation of an item that can be stripped down +/datum/strippable_item + /// The STRIPPABLE_ITEM_* key + var/key + + /// Should we warn about dangerous clothing? + var/warn_dangerous_clothing = TRUE + +/// Gets the item from the given source. +/datum/strippable_item/proc/get_item(atom/source) + +/// Tries to equip the item onto the given source. +/// Returns TRUE/FALSE depending on if it is allowed. +/// This should be used for checking if an item CAN be equipped. +/// It should not perform the equipping itself. +/datum/strippable_item/proc/try_equip(atom/source, obj/item/equipping, mob/user) + if ((equipping.flags_item & ITEM_ABSTRACT)) + return FALSE + if ((equipping.flags_item & NODROP)) + to_chat(user, SPAN_WARNING("You can't put [equipping] on [source], it's stuck to your hand!")) + return FALSE + if (ishuman(source)) + var/mob/living/carbon/human/sourcehuman = source + if(HAS_TRAIT(sourcehuman, TRAIT_UNSTRIPPABLE) && !sourcehuman.is_mob_incapacitated()) + to_chat(src, SPAN_DANGER("[sourcehuman] is too strong to force [equipping] onto them!")) + return + return TRUE + +/// Start the equipping process. This is the proc you should yield in. +/// Returns TRUE/FALSE depending on if it is allowed. +/datum/strippable_item/proc/start_equip(atom/source, obj/item/equipping, mob/user) + source.visible_message( + SPAN_NOTICE("[user] tries to put [equipping] on [source]."), + SPAN_NOTICE("[user] tries to put [equipping] on you.") + ) + + if (ismob(source)) + var/mob/sourcemob = source + sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] is having [equipping] put on them by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] [key_name(user)] is putting [equipping] on [key_name(sourcemob)]") + + return TRUE + +/// The proc that places the item on the source. This should not yield. +/datum/strippable_item/proc/finish_equip(atom/source, obj/item/equipping, mob/user) + SHOULD_NOT_SLEEP(TRUE) + +/// Tries to unequip the item from the given source. +/// Returns TRUE/FALSE depending on if it is allowed. +/// This should be used for checking if it CAN be unequipped. +/// It should not perform the unequipping itself. +/datum/strippable_item/proc/try_unequip(atom/source, mob/user) + SHOULD_NOT_SLEEP(TRUE) + + var/obj/item/item = get_item(source) + if (isnull(item)) + return FALSE + + if (user.action_busy && !skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED)) + to_chat(user, SPAN_WARNING("You can't do this right now.")) + return FALSE + + if ((item.flags_inventory & CANTSTRIP) || ((item.flags_item & NODROP) && !(item.flags_item & FORCEDROP_CONDITIONAL)) || (item.flags_item & ITEM_ABSTRACT)) + return FALSE + + if (ishuman(source)) + var/mob/living/carbon/human/sourcehuman = source + if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcehuman.stat == DEAD || sourcehuman.health < HEALTH_THRESHOLD_CRIT) && !sourcehuman.get_target_lock(user.faction_group)) + to_chat(user, SPAN_WARNING("You can't strip items of a crit or dead member of another faction!")) + return FALSE + + if(HAS_TRAIT(sourcehuman, TRAIT_UNSTRIPPABLE) && !sourcehuman.is_mob_incapacitated()) + to_chat(src, SPAN_DANGER("[sourcehuman] has an unbreakable grip on their equipment!")) + return + + return TRUE + +/// Start the unequipping process. This is the proc you should yield in. +/// Returns TRUE/FALSE depending on if it is allowed. +/datum/strippable_item/proc/start_unequip(atom/source, mob/user) + var/obj/item/item = get_item(source) + if (isnull(item)) + return FALSE + + source.visible_message( + SPAN_WARNING("[user] tries to remove [source]'s [item]."), + SPAN_DANGER("[user] tries to remove your [item].") + ) + + if (ismob(source)) + var/mob/sourcemob = source + sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] is being stripped of [item] by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] [key_name(user)] is stripping [key_name(sourcemob)] of [item]") + + item.add_fingerprint(user) + + return TRUE + +/// The proc that unequips the item from the source. This should not yield. +/datum/strippable_item/proc/finish_unequip(atom/source, mob/user) + +/// Returns a STRIPPABLE_OBSCURING_* define to report on whether or not this is obscured. +/datum/strippable_item/proc/get_obscuring(atom/source) + SHOULD_NOT_SLEEP(TRUE) + return STRIPPABLE_OBSCURING_NONE + +/// Returns the ID of this item's strippable action. +/// Return `null` if there is no alternate action. +/// Any return value of this must be in StripMenu. +/datum/strippable_item/proc/get_alternate_action(atom/source, mob/user) + return null + +/// Performs an alternative action on this strippable_item. +/// `has_alternate_action` needs to be TRUE. +/datum/strippable_item/proc/alternate_action(atom/source, mob/user) + +/// Returns whether or not this item should show. +/datum/strippable_item/proc/should_show(atom/source, mob/user) + return TRUE + +/// A preset for equipping items onto mob slots +/datum/strippable_item/mob_item_slot + /// The ITEM_SLOT_* to equip to. + var/item_slot + +/datum/strippable_item/proc/has_no_item_alt_action() + return FALSE + +/datum/strippable_item/mob_item_slot/get_item(atom/source) + if (!ismob(source)) + return null + + var/mob/mob_source = source + return mob_source.get_item_by_slot(key) + +/datum/strippable_item/mob_item_slot/try_equip(atom/source, obj/item/equipping, mob/user) + . = ..() + if (!.) + return + + if (!ismob(source)) + return FALSE + if (user.action_busy) + to_chat(user, SPAN_WARNING("You can't do this right now.")) + return FALSE + if (!equipping.mob_can_equip( + source, + key + )) + to_chat(user, SPAN_WARNING("\The [equipping] doesn't fit in that place!")) + return FALSE + if(equipping.flags_item & WIELDED) + equipping.unwield(user) + return TRUE + +/datum/strippable_item/mob_item_slot/start_equip(atom/source, obj/item/equipping, mob/user) + . = ..() + if (!.) + return + + if (!ismob(source)) + return FALSE + + var/time_to_strip = HUMAN_STRIP_DELAY + var/mob/sourcemob = source + + if (ishuman(sourcemob) && ishuman(user)) + var/mob/living/carbon/human/sourcehuman = sourcemob + var/mob/living/carbon/human/userhuman = user + time_to_strip = userhuman.get_strip_delay(userhuman, sourcehuman) + + if (!do_after(user, time_to_strip, INTERRUPT_ALL, BUSY_ICON_FRIENDLY, source, INTERRUPT_MOVED, BUSY_ICON_FRIENDLY)) + return FALSE + + if (!equipping.mob_can_equip( + sourcemob, + key + )) + return FALSE + + if (!user.temp_drop_inv_item(equipping)) + return FALSE + + return TRUE + +/datum/strippable_item/mob_item_slot/finish_equip(atom/source, obj/item/equipping, mob/user) + if (!ismob(source)) + return FALSE + + var/mob/sourcemob = source + sourcemob.equip_to_slot_if_possible(equipping, key) + +/datum/strippable_item/mob_item_slot/get_obscuring(atom/source) + return FALSE + +/datum/strippable_item/mob_item_slot/start_unequip(atom/source, mob/user) + . = ..() + if (!.) + return + + return start_unequip_mob(get_item(source), source, user) + +/datum/strippable_item/mob_item_slot/finish_unequip(atom/source, mob/user) + var/obj/item/item = get_item(source) + if (isnull(item)) + return FALSE + + if (!ismob(source)) + return FALSE + + return finish_unequip_mob(item, source, user) + +/// A utility function for `/datum/strippable_item`s to start unequipping an item from a mob. +/datum/strippable_item/mob_item_slot/proc/start_unequip_mob(obj/item/item, mob/living/carbon/human/source, mob/living/carbon/human/user) + var/time_to_strip = HUMAN_STRIP_DELAY + + if (istype(source) && istype(user)) + time_to_strip = user.get_strip_delay(user, source) + + if (!do_after(user, time_to_strip, INTERRUPT_ALL, BUSY_ICON_HOSTILE, source, INTERRUPT_MOVED, BUSY_ICON_HOSTILE)) + return FALSE + + return TRUE + +/// A utility function for `/datum/strippable_item`s to finish unequipping an item from a mob. +/datum/strippable_item/mob_item_slot/proc/finish_unequip_mob(obj/item/item, mob/source, mob/user) + if (!source.drop_inv_item_on_ground(item, force = (item.flags_item & FORCEDROP_CONDITIONAL))) //force if we can drop the item in this case + return FALSE + + if (ismob(source)) + var/mob/sourcemob = source + sourcemob.attack_log += text("\[[time_stamp()]\] [key_name(sourcemob)] has been stripped of [item] by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] [key_name(user)] has been stripped of [key_name(sourcemob)] of [item]") + + // Updates speed in case stripped speed affecting item + source.recalculate_move_delay = TRUE + +/// A representation of the stripping UI +/datum/strip_menu + /// The owner who has the element /datum/element/strippable + var/atom/movable/owner + + /// The strippable element itself + var/datum/element/strippable/strippable + + /// A lazy list of user mobs to a list of strip menu keys that they're interacting with + var/list/interactions + +/datum/strip_menu/New(atom/movable/owner, datum/element/strippable/strippable) + . = ..() + src.owner = owner + src.strippable = strippable + +/datum/strip_menu/Destroy() + owner = null + strippable = null + + return ..() + +/datum/strip_menu/tgui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "StripMenu") + ui.open() + + +/datum/strip_menu/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/inventory), + ) + +/datum/strip_menu/ui_data(mob/user) + var/list/data = list() + + var/list/items = list() + + for (var/strippable_key in strippable.items) + var/datum/strippable_item/item_data = strippable.items[strippable_key] + + if (!item_data.should_show(owner, user)) + continue + + var/list/result + + if(strippable_key in LAZYACCESS(interactions, user)) + LAZYSET(result, "interacting", TRUE) + + var/obscuring = item_data.get_obscuring(owner) + if (obscuring != STRIPPABLE_OBSCURING_NONE) + LAZYSET(result, "obscured", obscuring) + items[strippable_key] = result + continue + + var/obj/item/item = item_data.get_item(owner) + if (isnull(item)) + if (item_data.has_no_item_alt_action()) + LAZYINITLIST(result) + result["no_item_action"] = item_data.get_alternate_action(owner, user) + items[strippable_key] = result + continue + + LAZYINITLIST(result) + + result["icon"] = icon2base64(icon(item.icon, item.icon_state, frame = 1)) + result["name"] = item.name + result["alternate"] = item_data.get_alternate_action(owner, user) + + items[strippable_key] = result + + data["items"] = items + + // While most `\the`s are implicit, this one is not. + // In this case, `\The` would otherwise be used. + // This doesn't match with what it's used for, which is to say "Stripping the alien drone", + // as opposed to "Stripping The alien drone". + // Human names will still show without "the", as they are proper nouns. + data["name"] = "\the [owner]" + + return data + +/datum/strip_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return + + . = TRUE + + var/mob/user = ui.user + + switch (action) + if ("equip") + var/key = params["key"] + var/datum/strippable_item/strippable_item = strippable.items[key] + + if (isnull(strippable_item)) + return + + if (!strippable_item.should_show(owner, user)) + return + + if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY) + return + + var/item = strippable_item.get_item(owner) + if (!isnull(item)) + return + + var/obj/item/held_item = user.get_held_item() + if (isnull(held_item)) + return + + if (!strippable_item.try_equip(owner, held_item, user)) + return + + LAZYORASSOCLIST(interactions, user, key) + + // Yielding call + var/should_finish = strippable_item.start_equip(owner, held_item, user) + + LAZYREMOVEASSOC(interactions, user, key) + + if (!should_finish) + return + + if (QDELETED(src) || QDELETED(owner)) + return + + // They equipped an item in the meantime + if (!isnull(strippable_item.get_item(owner))) + return + + if (!user.Adjacent(owner)) + return + + strippable_item.finish_equip(owner, held_item, user) + if ("strip") + var/key = params["key"] + var/datum/strippable_item/strippable_item = strippable.items[key] + + if (isnull(strippable_item)) + return + + if (!strippable_item.should_show(owner, user)) + return + + if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY) + return + + var/item = strippable_item.get_item(owner) + if (isnull(item)) + return + + if (!strippable_item.try_unequip(owner, user)) + return + + LAZYORASSOCLIST(interactions, user, key) + + var/should_unequip = strippable_item.start_unequip(owner, user) + + LAZYREMOVEASSOC(interactions, user, key) + + // Yielding call + if (!should_unequip) + return + + if (QDELETED(src) || QDELETED(owner)) + return + + // They changed the item in the meantime + if (strippable_item.get_item(owner) != item) + return + + if (!user.Adjacent(owner)) + return + + strippable_item.finish_unequip(owner, user) + if ("alt") + var/key = params["key"] + var/datum/strippable_item/strippable_item = strippable.items[key] + + if (isnull(strippable_item)) + return + + if (!strippable_item.should_show(owner, user)) + return + + if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY) + return + + var/item = strippable_item.get_item(owner) + if (isnull(item) && !strippable_item.has_no_item_alt_action()) + return + + if (isnull(strippable_item.get_alternate_action(owner, user))) + return + + LAZYORASSOCLIST(interactions, user, key) + + // Potentially yielding + strippable_item.alternate_action(owner, user) + + LAZYREMOVEASSOC(interactions, user, key) + +/datum/strip_menu/ui_host(mob/user) + return owner + +/datum/strip_menu/ui_status(mob/user, datum/ui_state/state) + . = ..() + + if (isliving(user)) + var/mob/living/living_user = user + + if ( + . == UI_UPDATE \ + && user.stat == CONSCIOUS \ + && living_user.body_position == LYING_DOWN \ + && user.Adjacent(owner) + ) + return UI_INTERACTIVE + +/// Creates an assoc list of keys to /datum/strippable_item +/proc/create_strippable_list(types) + var/list/strippable_items = list() + + for (var/strippable_type in types) + var/datum/strippable_item/strippable_item = new strippable_type + strippable_items[strippable_item.key] = strippable_item + + return strippable_items diff --git a/code/game/objects/items/misc.dm b/code/game/objects/items/misc.dm index 1bdcab2ac6..6057f35818 100644 --- a/code/game/objects/items/misc.dm +++ b/code/game/objects/items/misc.dm @@ -56,13 +56,13 @@ ..() if(!gripped) user.visible_message(SPAN_NOTICE("[user] grips [src] tightly."), SPAN_NOTICE("You grip [src] tightly.")) - flags_item |= NODROP + flags_item |= NODROP|FORCEDROP_CONDITIONAL ADD_TRAIT(user, TRAIT_HOLDS_CANE, TRAIT_SOURCE_ITEM) user.AddComponent(/datum/component/footstep, 6, 35, 4, 1, "cane_step") gripped = TRUE else user.visible_message(SPAN_NOTICE("[user] loosens \his grip on [src]."), SPAN_NOTICE("You loosen your grip on [src].")) - flags_item &= ~NODROP + flags_item &= ~(NODROP|FORCEDROP_CONDITIONAL) REMOVE_TRAIT(user, TRAIT_HOLDS_CANE, TRAIT_SOURCE_ITEM) // Ideally, this would be something like a component added onto every mob that prioritizes certain sounds, such as stomping over canes. var/component = user.GetComponent(/datum/component/footstep) diff --git a/code/game/objects/items/weapons/twohanded.dm b/code/game/objects/items/weapons/twohanded.dm index be7571fa84..36e0ea702a 100644 --- a/code/game/objects/items/weapons/twohanded.dm +++ b/code/game/objects/items/weapons/twohanded.dm @@ -101,7 +101,7 @@ w_class = SIZE_HUGE icon_state = "offhand" name = "offhand" - flags_item = DELONDROP|TWOHANDED|WIELDED + flags_item = DELONDROP|TWOHANDED|WIELDED|CANTSTRIP /obj/item/weapon/twohanded/offhand/unwield(mob/user) if(flags_item & WIELDED) diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 8e19a19053..b887cff60c 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -341,3 +341,24 @@ GLOBAL_LIST_EMPTY(asset_datums) if (!item_filename) return . = list("[item_filename]" = SSassets.transport.get_asset_url(item_filename)) + +/datum/asset/simple/inventory + assets = list( + "inventory-glasses.png" = 'icons/ui_Icons/inventory/glasses.png', + "inventory-head.png" = 'icons/ui_Icons/inventory/head.png', + "inventory-neck.png" = 'icons/ui_Icons/inventory/neck.png', + "inventory-mask.png" = 'icons/ui_Icons/inventory/mask.png', + "inventory-ears.png" = 'icons/ui_Icons/inventory/ears.png', + "inventory-uniform.png" = 'icons/ui_Icons/inventory/uniform.png', + "inventory-suit.png" = 'icons/ui_Icons/inventory/suit.png', + "inventory-gloves.png" = 'icons/ui_Icons/inventory/gloves.png', + "inventory-hand_l.png" = 'icons/ui_Icons/inventory/hand_l.png', + "inventory-hand_r.png" = 'icons/ui_Icons/inventory/hand_r.png', + "inventory-shoes.png" = 'icons/ui_Icons/inventory/shoes.png', + "inventory-suit_storage.png" = 'icons/ui_Icons/inventory/suit_storage.png', + "inventory-id.png" = 'icons/ui_Icons/inventory/id.png', + "inventory-belt.png" = 'icons/ui_Icons/inventory/belt.png', + "inventory-back.png" = 'icons/ui_Icons/inventory/back.png', + "inventory-pocket.png" = 'icons/ui_Icons/inventory/pocket.png', + "inventory-collar.png" = 'icons/ui_Icons/inventory/collar.png', + ) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 4ea2f35aa6..2a419eddf1 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -399,23 +399,6 @@ bodytemperature = max(bodytemperature, BODYTEMP_HEAT_DAMAGE_LIMIT+10) recalculate_move_delay = TRUE - -/mob/living/carbon/show_inv(mob/living/carbon/user as mob) - user.set_interaction(src) - var/dat = {" -
[name]
-

-
Head(Mask): [(wear_mask ? wear_mask : "Nothing")] -
Left Hand: [(l_hand ? l_hand : "Nothing")] -
Right Hand: [(r_hand ? r_hand : "Nothing")] -
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? " Set Internal" : "")] -
[(handcuffed ? "Handcuffed" : "Not Handcuffed")] -
[(internal ? "Remove Internal" : "")] -
Refresh -
Close -
"} - show_browser(user, dat, name, "mob[name]") - /** * Called by [/mob/dead/observer/proc/do_observe] when a carbon mob is observed by a ghost with [/datum/preferences/var/auto_observe] enabled. * diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 350bcec3e8..c394431c72 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -13,7 +13,7 @@ create_reagents(1000) if(!real_name || !name) change_real_name(src, "unknown") - + AddElement(/datum/element/strippable, GLOB.strippable_human_items, TYPE_PROC_REF(/mob/living/carbon/human, should_strip)) . = ..() prev_gender = gender // Debug for plural genders @@ -270,49 +270,6 @@ return TRUE return FALSE - - -/mob/living/carbon/human/show_inv(mob/living/user) - if(ismaintdrone(user)) - return - var/obj/item/clothing/under/suit = null - if(istype(w_uniform, /obj/item/clothing/under)) - suit = w_uniform - - user.set_interaction(src) - var/dat = {" -
[name]
-

-
(Exo)Suit: [(wear_suit ? wear_suit : "Nothing")] -
Suit Storage: [(s_store ? s_store : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(s_store, /obj/item/tank) && !( internal )) ? " Set Internal" : "")] -
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? " Set Internal" : "")] -
Head(Mask): [(wear_mask ? wear_mask : "Nothing")] -
Left Hand: [(l_hand ? l_hand : "Nothing")] -
Right Hand: [(r_hand ? r_hand : "Nothing")] -
Gloves: [(gloves ? gloves : "Nothing")] -
Eyes: [(glasses ? glasses : "Nothing")] -
Left Ear: [(wear_l_ear ? wear_l_ear : "Nothing")] -
Right Ear: [(wear_r_ear ? wear_r_ear : "Nothing")] -
Head: [(head ? head : "Nothing")] -
Shoes: [(shoes ? shoes : "Nothing")] -
Belt: [(belt ? belt : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(belt, /obj/item/tank) && !internal) ? " Set Internal" : "")] -
Uniform: [(w_uniform ? w_uniform : "Nothing")] [(suit) ? ((suit.has_sensor == UNIFORM_HAS_SENSORS) ? " Sensors" : "") : null] -
ID: [(wear_id ? wear_id : "Nothing")] -
Left Pocket: [(l_store ? l_store : "Nothing")] -
Right Pocket: [(r_store ? r_store : "Nothing")] -
- [handcuffed ? "
Handcuffed" : ""] - [legcuffed ? "
Legcuffed" : ""] - [suit && LAZYLEN(suit.accessories) ? "
Remove Accessory" : ""] - [internal ? "
Remove Internal" : ""] - [istype(wear_id, /obj/item/card/id/dogtag) ? "
Retrieve Info Tag" : ""] -
Remove Splints -
-
Refresh -
Close -
"} - show_browser(user, dat, name, "mob[name]") - /** * Handles any storage containers that the human is looking inside when auto-observed. */ @@ -428,9 +385,6 @@ /mob/living/carbon/human/Topic(href, href_list) - if(href_list["refresh"]) - if(interactee&&(in_range(src, usr))) - show_inv(interactee) if(href_list["mach_close"]) var/t1 = text("window=[]", href_list["mach_close"]) @@ -475,76 +429,6 @@ what = usr.get_active_hand() usr.stripPanelEquip(what,src,slot) - if(href_list["internal"]) - - if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr)) - attack_log += text("\[[time_stamp()]\] Has had their internals toggled by [key_name(usr)]") - usr.attack_log += text("\[[time_stamp()]\] Attempted to toggle [key_name(src)]'s' internals") - if(internal) - usr.visible_message(SPAN_DANGER("[usr] is trying to disable [src]'s internals"), null, null, 3) - else - usr.visible_message(SPAN_DANGER("[usr] is trying to enable [src]'s internals."), null, null, 3) - - if(do_after(usr, POCKET_STRIP_DELAY, INTERRUPT_ALL, BUSY_ICON_GENERIC, src, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) - if(internal) - internal.add_fingerprint(usr) - internal = null - visible_message("[src] is no longer running on internals.", null, null, 1) - else - if(istype(wear_mask, /obj/item/clothing/mask)) - if(istype(back, /obj/item/tank)) - internal = back - else if(istype(s_store, /obj/item/tank)) - internal = s_store - else if(istype(belt, /obj/item/tank)) - internal = belt - if(internal) - visible_message(SPAN_NOTICE("[src] is now running on internals."), null, null, 1) - internal.add_fingerprint(usr) - - // Update strip window - if(usr.interactee == src && Adjacent(usr)) - show_inv(usr) - - - if(href_list["splints"]) - if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr)) - if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group)) - to_chat(usr, SPAN_WARNING("You can't strip a crit or dead member of another faction!")) - return - attack_log += text("\[[time_stamp()]\] Has had their splints removed by [key_name(usr)]") - usr.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(src)]'s' splints ") - remove_splints(usr) - - if(href_list["tie"]) - if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr)) - if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group)) - to_chat(usr, SPAN_WARNING("You can't strip a crit or dead member of another faction!")) - return - if(w_uniform && istype(w_uniform, /obj/item/clothing)) - var/obj/item/clothing/under/U = w_uniform - if(!LAZYLEN(U.accessories)) - return FALSE - var/obj/item/clothing/accessory/A = LAZYACCESS(U.accessories, 1) - if(LAZYLEN(U.accessories) > 1) - A = tgui_input_list(usr, "Select an accessory to remove from [U]", "Remove accessory", U.accessories) - if(!istype(A)) - return - attack_log += text("\[[time_stamp()]\] Has had their accessory ([A]) removed by [key_name(usr)]") - usr.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(src)]'s' accessory ([A])") - if(istype(A, /obj/item/clothing/accessory/holobadge) || istype(A, /obj/item/clothing/accessory/medal)) - visible_message(SPAN_DANGER("[usr] tears off \the [A] from [src]'s [U]!"), null, null, 5) - if(U == w_uniform) - U.remove_accessory(usr, A) - else - if(HAS_TRAIT(src, TRAIT_UNSTRIPPABLE) && !is_mob_incapacitated()) //Can't strip the unstrippable! - to_chat(usr, SPAN_DANGER("[src] has an unbreakable grip on their equipment!")) - return - visible_message(SPAN_DANGER("[usr] is trying to take off \a [A] from [src]'s [U]!"), null, null, 5) - if(do_after(usr, get_strip_delay(usr, src), INTERRUPT_ALL, BUSY_ICON_GENERIC, src, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) - if(U == w_uniform) - U.remove_accessory(usr, A) - if(href_list["sensor"]) if(!usr.action_busy && !usr.is_mob_incapacitated() && Adjacent(usr)) if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (stat == DEAD || health < HEALTH_THRESHOLD_CRIT) && !get_target_lock(usr.faction_group)) diff --git a/code/modules/mob/living/carbon/human/human_stripping.dm b/code/modules/mob/living/carbon/human/human_stripping.dm new file mode 100644 index 0000000000..fbf6fb5ce4 --- /dev/null +++ b/code/modules/mob/living/carbon/human/human_stripping.dm @@ -0,0 +1,272 @@ +GLOBAL_LIST_INIT(strippable_human_items, create_strippable_list(list( + /datum/strippable_item/mob_item_slot/head, + /datum/strippable_item/mob_item_slot/back, + /datum/strippable_item/mob_item_slot/mask, + /datum/strippable_item/mob_item_slot/eyes, + /datum/strippable_item/mob_item_slot/r_ear, + /datum/strippable_item/mob_item_slot/l_ear, + /datum/strippable_item/mob_item_slot/jumpsuit, + /datum/strippable_item/mob_item_slot/suit, + /datum/strippable_item/mob_item_slot/gloves, + /datum/strippable_item/mob_item_slot/feet, + /datum/strippable_item/mob_item_slot/suit_storage, + /datum/strippable_item/mob_item_slot/id, + /datum/strippable_item/mob_item_slot/belt, + /datum/strippable_item/mob_item_slot/pocket/left, + /datum/strippable_item/mob_item_slot/pocket/right, + /datum/strippable_item/mob_item_slot/hand/left, + /datum/strippable_item/mob_item_slot/hand/right, + /datum/strippable_item/mob_item_slot/cuffs/handcuffs, + /datum/strippable_item/mob_item_slot/cuffs/legcuffs, +))) + +/mob/living/carbon/human/proc/should_strip(mob/user) + if (user.pulling == src && user.grab_level == GRAB_AGGRESSIVE && (user.a_intent & INTENT_GRAB)) + return FALSE //to not interfere with fireman carry + return TRUE + +/datum/strippable_item/mob_item_slot/head + key = STRIPPABLE_ITEM_HEAD + item_slot = SLOT_HEAD + +/datum/strippable_item/mob_item_slot/back + key = STRIPPABLE_ITEM_BACK + item_slot = SLOT_BACK + +/datum/strippable_item/mob_item_slot/mask + key = STRIPPABLE_ITEM_MASK + item_slot = SLOT_FACE + +/datum/strippable_item/mob_item_slot/mask/get_alternate_action(atom/source, mob/user) + var/obj/item/clothing/mask = get_item(source) + if (!istype(mask)) + return + if (!ishuman(source)) + return + var/mob/living/carbon/human/sourcehuman = source + if (istype(sourcehuman.s_store, /obj/item/tank)) + return "toggle_internals" + if (istype(sourcehuman.back, /obj/item/tank)) + return "toggle_internals" + if (istype(sourcehuman.belt, /obj/item/tank)) + return "toggle_internals" + return + +/datum/strippable_item/mob_item_slot/mask/alternate_action(atom/source, mob/user) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcehuman = source + if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user)) + return + if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcehuman.stat == DEAD || sourcehuman.health < HEALTH_THRESHOLD_CRIT) && !sourcehuman.get_target_lock(user.faction_group)) + to_chat(user, SPAN_WARNING("You can't toggle internals of a crit or dead member of another faction!")) + return + + sourcehuman.attack_log += text("\[[time_stamp()]\] Has had their internals toggled by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] Attempted to toggle [key_name(src)]'s' internals") + if(sourcehuman.internal) + user.visible_message(SPAN_DANGER("[user] is trying to disable [sourcehuman]'s internals"), null, null, 3) + else + user.visible_message(SPAN_DANGER("[user] is trying to enable [sourcehuman]'s internals."), null, null, 3) + + if(!do_after(user, POCKET_STRIP_DELAY, INTERRUPT_ALL, BUSY_ICON_GENERIC, sourcehuman, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) + return + + if(sourcehuman.internal) + sourcehuman.internal.add_fingerprint(user) + sourcehuman.internal = null + sourcehuman.visible_message("[sourcehuman] is no longer running on internals.", max_distance = 1) + return + + if(!istype(sourcehuman.wear_mask, /obj/item/clothing/mask)) + return + + if(istype(sourcehuman.back, /obj/item/tank)) + sourcehuman.internal = sourcehuman.back + else if(istype(sourcehuman.s_store, /obj/item/tank)) + sourcehuman.internal = sourcehuman.s_store + else if(istype(sourcehuman.belt, /obj/item/tank)) + sourcehuman.internal = sourcehuman.belt + + if(!sourcehuman.internal) + return + + sourcehuman.visible_message(SPAN_NOTICE("[sourcehuman] is now running on internals."), max_distance = 1) + sourcehuman.internal.add_fingerprint(user) + +/datum/strippable_item/mob_item_slot/eyes + key = STRIPPABLE_ITEM_EYES + item_slot = SLOT_EYES + +/datum/strippable_item/mob_item_slot/r_ear + key = STRIPPABLE_ITEM_R_EAR + item_slot = SLOT_EAR + +/datum/strippable_item/mob_item_slot/l_ear + key = STRIPPABLE_ITEM_L_EAR + item_slot = SLOT_EAR + +/datum/strippable_item/mob_item_slot/jumpsuit + key = STRIPPABLE_ITEM_JUMPSUIT + item_slot = SLOT_ICLOTHING + +/datum/strippable_item/mob_item_slot/jumpsuit/get_alternate_action(atom/source, mob/user) + var/obj/item/clothing/under/uniform = get_item(source) + if (!istype(uniform)) + return null + return uniform?.accessories ? "remove_accessory" : null + +/datum/strippable_item/mob_item_slot/jumpsuit/alternate_action(atom/source, mob/user) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcemob = source + if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user)) + return + if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group)) + to_chat(user, SPAN_WARNING("You can't strip a crit or dead member of another faction!")) + return + if(!sourcemob.w_uniform || !istype(sourcemob.w_uniform, /obj/item/clothing)) + return + + var/obj/item/clothing/under/uniform = sourcemob.w_uniform + if(!LAZYLEN(uniform.accessories)) + return FALSE + var/obj/item/clothing/accessory/accessory = LAZYACCESS(uniform.accessories, 1) + if(LAZYLEN(uniform.accessories) > 1) + accessory = tgui_input_list(user, "Select an accessory to remove from [uniform]", "Remove accessory", uniform.accessories) + if(!istype(accessory)) + return + sourcemob.attack_log += text("\[[time_stamp()]\] Has had their accessory ([accessory]) removed by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(sourcemob)]'s' accessory ([accessory])") + if(istype(accessory, /obj/item/clothing/accessory/holobadge) || istype(accessory, /obj/item/clothing/accessory/medal)) + sourcemob.visible_message(SPAN_DANGER("[user] tears off [accessory] from [sourcemob]'s [uniform]!"), null, null, 5) + if(uniform == sourcemob.w_uniform) + uniform.remove_accessory(user, accessory) + return + + if(HAS_TRAIT(sourcemob, TRAIT_UNSTRIPPABLE) && !sourcemob.is_mob_incapacitated()) //Can't strip the unstrippable! + to_chat(user, SPAN_DANGER("[sourcemob] has an unbreakable grip on their equipment!")) + return + sourcemob.visible_message(SPAN_DANGER("[user] is trying to take off \a [accessory] from [source]'s [uniform]!"), null, null, 5) + + if(!do_after(user, sourcemob.get_strip_delay(user, sourcemob), INTERRUPT_ALL, BUSY_ICON_GENERIC, sourcemob, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) + return + + if(uniform != sourcemob.w_uniform) + return + + uniform.remove_accessory(user, accessory) + +/datum/strippable_item/mob_item_slot/suit + key = STRIPPABLE_ITEM_SUIT + item_slot = SLOT_OCLOTHING + +/datum/strippable_item/mob_item_slot/suit/has_no_item_alt_action() + return TRUE + +/datum/strippable_item/mob_item_slot/suit/get_alternate_action(atom/source, mob/user) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcemob = source + for(var/bodypart in list("l_leg","r_leg","l_arm","r_arm","r_hand","l_hand","r_foot","l_foot","chest","head","groin")) + var/obj/limb/limb = sourcemob.get_limb(bodypart) + if(limb && (limb.status & LIMB_SPLINTED)) + return "remove_splints" + return + +/datum/strippable_item/mob_item_slot/suit/alternate_action(atom/source, mob/user) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcemob = source + if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user)) + return + if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group)) + to_chat(user, SPAN_WARNING("You can't remove splints of a crit or dead member of another faction!")) + return + sourcemob.attack_log += text("\[[time_stamp()]\] Has had their splints removed by [key_name(user)]") + user.attack_log += text("\[[time_stamp()]\] Attempted to remove [key_name(sourcemob)]'s' splints ") + sourcemob.remove_splints(user) + +/datum/strippable_item/mob_item_slot/gloves + key = STRIPPABLE_ITEM_GLOVES + item_slot = SLOT_HANDS + +/datum/strippable_item/mob_item_slot/feet + key = STRIPPABLE_ITEM_FEET + item_slot = SLOT_FEET + +/datum/strippable_item/mob_item_slot/suit_storage + key = STRIPPABLE_ITEM_SUIT_STORAGE + item_slot = SLOT_SUIT_STORE + +/datum/strippable_item/mob_item_slot/id + key = STRIPPABLE_ITEM_ID + item_slot = SLOT_ID + +/datum/strippable_item/mob_item_slot/id/get_alternate_action(atom/source, mob/user) + var/obj/item/card/id/dogtag/tag = get_item(source) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcemob = source + if (!istype(tag)) + return + if (!sourcemob.undefibbable && (!skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED) || sourcemob.stat != DEAD)) + return + return tag.dogtag_taken ? null : "retrieve_tag" + +/datum/strippable_item/mob_item_slot/id/alternate_action(atom/source, mob/user) + if(!ishuman(source)) + return + var/mob/living/carbon/human/sourcemob = source + if(user.action_busy || user.is_mob_incapacitated() || !source.Adjacent(user)) + return + if(MODE_HAS_TOGGLEABLE_FLAG(MODE_NO_STRIPDRAG_ENEMY) && (sourcemob.stat == DEAD || sourcemob.health < HEALTH_THRESHOLD_CRIT) && !sourcemob.get_target_lock(user.faction_group)) + to_chat(user, SPAN_WARNING("You can't strip a crit or dead member of another faction!")) + return + if(!istype(sourcemob.wear_id, /obj/item/card/id/dogtag)) + return + if (!sourcemob.undefibbable && !skillcheck(user, SKILL_POLICE, SKILL_POLICE_SKILLED)) + return + var/obj/item/card/id/dogtag/tag = sourcemob.wear_id + if(tag.dogtag_taken) + to_chat(user, SPAN_WARNING("Someone's already taken [sourcemob]'s information tag.")) + return + + if(sourcemob.stat != DEAD) + to_chat(user, SPAN_WARNING("You can't take a dogtag's information tag while its owner is alive.")) + return + + to_chat(user, SPAN_NOTICE("You take [sourcemob]'s information tag, leaving the ID tag")) + tag.dogtag_taken = TRUE + tag.icon_state = "dogtag_taken" + var/obj/item/dogtag/newtag = new(sourcemob.loc) + newtag.fallen_names = list(tag.registered_name) + newtag.fallen_assgns = list(tag.assignment) + newtag.fallen_blood_types = list(tag.blood_type) + user.put_in_hands(newtag) + + + +/datum/strippable_item/mob_item_slot/belt + key = STRIPPABLE_ITEM_BELT + item_slot = SLOT_WAIST + +/datum/strippable_item/mob_item_slot/pocket/left + key = STRIPPABLE_ITEM_LPOCKET + item_slot = SLOT_STORE + +/datum/strippable_item/mob_item_slot/pocket/right + key = STRIPPABLE_ITEM_RPOCKET + item_slot = SLOT_STORE + +/datum/strippable_item/mob_item_slot/hand/left + key = STRIPPABLE_ITEM_LHAND + +/datum/strippable_item/mob_item_slot/hand/right + key = STRIPPABLE_ITEM_RHAND + +/datum/strippable_item/mob_item_slot/cuffs/handcuffs + key = STRIPPABLE_ITEM_HANDCUFFS + +/datum/strippable_item/mob_item_slot/cuffs/legcuffs + key = STRIPPABLE_ITEM_LEGCUFFS diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index 22deabce45..871a2cd376 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -504,68 +504,6 @@ /// Final result is overall delay * speed multiplier return target_delay * user_speed -/mob/living/carbon/human/stripPanelUnequip(obj/item/interact_item, mob/target_mob, slot_to_process) - if(HAS_TRAIT(target_mob, TRAIT_UNSTRIPPABLE) && !target_mob.is_mob_incapacitated()) //Can't strip the unstrippable! - to_chat(src, SPAN_DANGER("[target_mob] has an unbreakable grip on their equipment!")) - return - if(interact_item.flags_item & ITEM_ABSTRACT) - return - if(interact_item.flags_item & NODROP) - to_chat(src, SPAN_WARNING("You can't remove \the [interact_item.name], it appears to be stuck!")) - return - if(interact_item.flags_inventory & CANTSTRIP) - to_chat(src, SPAN_WARNING("You're having difficulty removing \the [interact_item.name].")) - return - target_mob.attack_log += "\[[time_stamp()]\] Has had their [interact_item.name] ([slot_to_process]) attempted to be removed by [key_name(src)]" - attack_log += "\[[time_stamp()]\] Attempted to remove [key_name(target_mob)]'s [interact_item.name] ([slot_to_process])" - log_interact(src, target_mob, "[key_name(src)] tried to remove [key_name(target_mob)]'s [interact_item.name] ([slot_to_process]).") - - src.visible_message(SPAN_DANGER("[src] tries to remove [target_mob]'s [interact_item.name]."), \ - SPAN_DANGER("You are trying to remove [target_mob]'s [interact_item.name]."), null, 5) - interact_item.add_fingerprint(src) - if(do_after(src, get_strip_delay(src, target_mob), INTERRUPT_ALL, BUSY_ICON_GENERIC, target_mob, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) - if(interact_item && Adjacent(target_mob) && interact_item == target_mob.get_item_by_slot(slot_to_process)) - target_mob.drop_inv_item_on_ground(interact_item) - log_interact(src, target_mob, "[key_name(src)] removed [key_name(target_mob)]'s [interact_item.name] ([slot_to_process]) successfully.") - - if(target_mob) - if(interactee == target_mob && Adjacent(target_mob)) - target_mob.show_inv(src) - - -/mob/living/carbon/human/stripPanelEquip(obj/item/interact_item, mob/target_mob, slot_to_process) - if(HAS_TRAIT(target_mob, TRAIT_UNSTRIPPABLE) && !target_mob.is_mob_incapacitated()) - to_chat(src, SPAN_DANGER("[target_mob] is too strong to force [interact_item.name] onto them!")) - return - if(interact_item && !(interact_item.flags_item & ITEM_ABSTRACT)) - if(interact_item.flags_item & NODROP) - to_chat(src, SPAN_WARNING("You can't put \the [interact_item.name] on [target_mob], it's stuck to your hand!")) - return - if(interact_item.flags_inventory & CANTSTRIP) - to_chat(src, SPAN_WARNING("You're having difficulty putting \the [interact_item.name] on [target_mob].")) - return - if(interact_item.flags_item & WIELDED) - interact_item.unwield(src) - if(!interact_item.mob_can_equip(target_mob, slot_to_process, TRUE)) - to_chat(src, SPAN_WARNING("You can't put \the [interact_item.name] on [target_mob]!")) - return - visible_message(SPAN_NOTICE("[src] tries to put \the [interact_item.name] on [target_mob]."), null, null, 5) - if(do_after(src, get_strip_delay(src, target_mob), INTERRUPT_ALL, BUSY_ICON_GENERIC, target_mob, INTERRUPT_MOVED, BUSY_ICON_GENERIC)) - if(interact_item == get_active_hand() && !target_mob.get_item_by_slot(slot_to_process) && Adjacent(target_mob)) - if(interact_item.flags_item & WIELDED) //to prevent re-wielding it during the do_after - interact_item.unwield(src) - if(interact_item.mob_can_equip(target_mob, slot_to_process, TRUE))//Placing an item on the mob - drop_inv_item_on_ground(interact_item) - if(interact_item && !QDELETED(interact_item)) //Might be self-deleted? - target_mob.equip_to_slot_if_possible(interact_item, slot_to_process, 1, 0, 1, 1) - if(ishuman(target_mob) && target_mob.stat == DEAD) - var/mob/living/carbon/human/human_target = target_mob - human_target.disable_lights() // take that powergamers -spookydonut - - if(target_mob) - if(interactee == target_mob && Adjacent(target_mob)) - target_mob.show_inv(src) - /mob/living/carbon/human/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force) remember_dropped_object(I) return ..() diff --git a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm index 8d3367ee68..d00b08f183 100644 --- a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm +++ b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm @@ -261,10 +261,6 @@ move_delay = . - -/mob/living/carbon/xenomorph/show_inv(mob/user) - return - /mob/living/carbon/xenomorph/proc/pounced_mob(mob/living/L) // This should only be called back by a mob that has pounce, so no need to check var/datum/action/xeno_action/activable/pounce/pounceAction = get_xeno_action_by_type(src, /datum/action/xeno_action/activable/pounce) diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index f1452d95c2..78c96bbbb7 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -110,19 +110,6 @@ /* * Inventory */ -/mob/living/simple_animal/parrot/show_inv(mob/user as mob) - user.set_interaction(src) - if(user.stat) return - - var/dat = "
Inventory of [name]

" - if(ears) - dat += "
Headset: [ears] (Remove)" - else - dat += "
Headset: Nothing" - - user << browse(dat, text("window=mob[];size=325x500", name)) - onclose(user, "mob[real_name]") - return /mob/living/simple_animal/parrot/Topic(href, href_list) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index c2cf6402ed..13c47719a7 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -372,25 +372,6 @@ SIGNAL_HANDLER reset_view(null) -/mob/proc/show_inv(mob/user) - user.set_interaction(src) - var/dat = {" -


[name] -

-
Head(Mask): [(wear_mask ? wear_mask : "Nothing")] -
Left Hand: [(l_hand ? l_hand : "Nothing")] -
Right Hand: [(r_hand ? r_hand : "Nothing")] -
Back: [(back ? back : "Nothing")] [((istype(wear_mask, /obj/item/clothing/mask) && istype(back, /obj/item/tank) && !( internal )) ? text(" Set Internal", src) : "")] -
[(internal ? text("Remove Internal") : "")] -
Empty Pockets -
Refresh -
Close -
"} - show_browser(user, dat, name, "mob[name]") - return - - - /mob/proc/point_to_atom(atom/A, turf/T) //Squad Leaders and above have reduced cooldown and get a bigger arrow if(check_improved_pointing()) @@ -448,21 +429,6 @@ update_flavor_text() return - -/mob/MouseDrop(mob/M) - ..() - if(M != usr) return - if(usr == src) return - if(!Adjacent(usr)) return - if(!ishuman(M) && !ismonkey(M)) return - if(!ishuman(src) && !ismonkey(src)) return - if(M.is_mob_incapacitated()) - return - if(M.pulling == src && (M.a_intent & INTENT_GRAB) && M.grab_level == GRAB_AGGRESSIVE) - return - - show_inv(M) - /mob/proc/swap_hand() hand = !hand diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm index 0c6da6f973..6f972bdeb8 100644 --- a/code/modules/projectiles/gun_attachables.dm +++ b/code/modules/projectiles/gun_attachables.dm @@ -2445,7 +2445,7 @@ Defined in conflicts.dm of the #defines folder. /obj/item/attachable/stock/smg/collapsible/brace/apply_on_weapon(obj/item/weapon/gun/G) if(stock_activated) - G.flags_item |= NODROP + G.flags_item |= NODROP|FORCEDROP_CONDITIONAL accuracy_mod = -HIT_ACCURACY_MULT_TIER_3 scatter_mod = SCATTER_AMOUNT_TIER_8 recoil_mod = RECOIL_AMOUNT_TIER_2 //Hurts pretty bad if it's wielded. @@ -2456,7 +2456,7 @@ Defined in conflicts.dm of the #defines folder. icon_state = "smg_brace_on" attach_icon = "smg_brace_a_on" else - G.flags_item &= ~NODROP + G.flags_item &= ~(NODROP|FORCEDROP_CONDITIONAL) accuracy_mod = 0 scatter_mod = 0 recoil_mod = 0 diff --git a/colonialmarines.dme b/colonialmarines.dme index d5e035d6d2..fa6152a276 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -101,6 +101,7 @@ #include "code\__DEFINES\stamina.dm" #include "code\__DEFINES\stats.dm" #include "code\__DEFINES\status_effects.dm" +#include "code\__DEFINES\strippable.dm" #include "code\__DEFINES\STUI.dm" #include "code\__DEFINES\subsystems.dm" #include "code\__DEFINES\surgery.dm" @@ -481,6 +482,7 @@ #include "code\datums\elements\light_blocking.dm" #include "code\datums\elements\mouth_drop_item.dm" #include "code\datums\elements\poor_eyesight_correction.dm" +#include "code\datums\elements\strippable.dm" #include "code\datums\elements\suturing.dm" #include "code\datums\elements\yautja_tracked_item.dm" #include "code\datums\elements\bullet_trait\damage_boost.dm" @@ -1911,6 +1913,7 @@ #include "code\modules\mob\living\carbon\human\human_dummy.dm" #include "code\modules\mob\living\carbon\human\human_helpers.dm" #include "code\modules\mob\living\carbon\human\human_movement.dm" +#include "code\modules\mob\living\carbon\human\human_stripping.dm" #include "code\modules\mob\living\carbon\human\inventory.dm" #include "code\modules\mob\living\carbon\human\life.dm" #include "code\modules\mob\living\carbon\human\login.dm" diff --git a/icons/ui_icons/inventory/back.png b/icons/ui_icons/inventory/back.png new file mode 100644 index 0000000000..736b9d64bf Binary files /dev/null and b/icons/ui_icons/inventory/back.png differ diff --git a/icons/ui_icons/inventory/belt.png b/icons/ui_icons/inventory/belt.png new file mode 100644 index 0000000000..1be89d450a Binary files /dev/null and b/icons/ui_icons/inventory/belt.png differ diff --git a/icons/ui_icons/inventory/collar.png b/icons/ui_icons/inventory/collar.png new file mode 100644 index 0000000000..71803b1b6c Binary files /dev/null and b/icons/ui_icons/inventory/collar.png differ diff --git a/icons/ui_icons/inventory/ears.png b/icons/ui_icons/inventory/ears.png new file mode 100644 index 0000000000..e9a8f3c23c Binary files /dev/null and b/icons/ui_icons/inventory/ears.png differ diff --git a/icons/ui_icons/inventory/glasses.png b/icons/ui_icons/inventory/glasses.png new file mode 100644 index 0000000000..6e6f1ad098 Binary files /dev/null and b/icons/ui_icons/inventory/glasses.png differ diff --git a/icons/ui_icons/inventory/gloves.png b/icons/ui_icons/inventory/gloves.png new file mode 100644 index 0000000000..2c8a16cbdb Binary files /dev/null and b/icons/ui_icons/inventory/gloves.png differ diff --git a/icons/ui_icons/inventory/hand_l.png b/icons/ui_icons/inventory/hand_l.png new file mode 100644 index 0000000000..b09228d65f Binary files /dev/null and b/icons/ui_icons/inventory/hand_l.png differ diff --git a/icons/ui_icons/inventory/hand_r.png b/icons/ui_icons/inventory/hand_r.png new file mode 100644 index 0000000000..0e05a487e0 Binary files /dev/null and b/icons/ui_icons/inventory/hand_r.png differ diff --git a/icons/ui_icons/inventory/head.png b/icons/ui_icons/inventory/head.png new file mode 100644 index 0000000000..11e2d2254c Binary files /dev/null and b/icons/ui_icons/inventory/head.png differ diff --git a/icons/ui_icons/inventory/id.png b/icons/ui_icons/inventory/id.png new file mode 100644 index 0000000000..4469591d36 Binary files /dev/null and b/icons/ui_icons/inventory/id.png differ diff --git a/icons/ui_icons/inventory/mask.png b/icons/ui_icons/inventory/mask.png new file mode 100644 index 0000000000..82e5108937 Binary files /dev/null and b/icons/ui_icons/inventory/mask.png differ diff --git a/icons/ui_icons/inventory/neck.png b/icons/ui_icons/inventory/neck.png new file mode 100644 index 0000000000..78ad3ce3b1 Binary files /dev/null and b/icons/ui_icons/inventory/neck.png differ diff --git a/icons/ui_icons/inventory/pocket.png b/icons/ui_icons/inventory/pocket.png new file mode 100644 index 0000000000..f42399dca0 Binary files /dev/null and b/icons/ui_icons/inventory/pocket.png differ diff --git a/icons/ui_icons/inventory/shoes.png b/icons/ui_icons/inventory/shoes.png new file mode 100644 index 0000000000..d20f7ef4d1 Binary files /dev/null and b/icons/ui_icons/inventory/shoes.png differ diff --git a/icons/ui_icons/inventory/suit.png b/icons/ui_icons/inventory/suit.png new file mode 100644 index 0000000000..e9c48e8069 Binary files /dev/null and b/icons/ui_icons/inventory/suit.png differ diff --git a/icons/ui_icons/inventory/suit_storage.png b/icons/ui_icons/inventory/suit_storage.png new file mode 100644 index 0000000000..9722eb1029 Binary files /dev/null and b/icons/ui_icons/inventory/suit_storage.png differ diff --git a/icons/ui_icons/inventory/uniform.png b/icons/ui_icons/inventory/uniform.png new file mode 100644 index 0000000000..292b3324b5 Binary files /dev/null and b/icons/ui_icons/inventory/uniform.png differ diff --git a/tgui/packages/tgui/interfaces/StripMenu.tsx b/tgui/packages/tgui/interfaces/StripMenu.tsx new file mode 100644 index 0000000000..acb4086c82 --- /dev/null +++ b/tgui/packages/tgui/interfaces/StripMenu.tsx @@ -0,0 +1,382 @@ +import { range } from 'common/collections'; +import { BooleanLike } from 'common/react'; + +import { resolveAsset } from '../assets'; +import { useBackend } from '../backend'; +import { Box, Button, Icon, Stack } from '../components'; +import { Window } from '../layouts'; + +const ROWS = 5; +const COLUMNS = 6; + +const BUTTON_DIMENSIONS = '50px'; + +type GridSpotKey = string; + +const getGridSpotKey = (spot: [number, number]): GridSpotKey => { + return `${spot[0]}/${spot[1]}`; +}; + +const CornerText = (props: { + readonly align: 'left' | 'right'; + readonly children: string; +}): JSX.Element => { + const { align, children } = props; + + return ( + + {children} + + ); +}; + +type AlternateAction = { + icon: string; + text: string; +}; + +const ALTERNATE_ACTIONS: Record = { + remove_splints: { + icon: 'crutch', + text: 'Remove splints', + }, + + remove_accessory: { + icon: 'tshirt', + text: 'Remove accessory', + }, + + retrieve_tag: { + icon: 'tags', + text: 'Retrieve info tag', + }, + + toggle_internals: { + icon: 'mask-face', + text: 'Toggle internals', + }, +}; + +type Slot = { + displayName: string; + gridSpot: GridSpotKey; + image?: string; + additionalComponent?: JSX.Element; + hideEmpty?: boolean; +}; + +const SLOTS: Record = { + glasses: { + displayName: 'glasses', + gridSpot: getGridSpotKey([0, 1]), + image: 'inventory-glasses.png', + }, + + head: { + displayName: 'headwear', + gridSpot: getGridSpotKey([0, 2]), + image: 'inventory-head.png', + }, + + wear_mask: { + displayName: 'mask', + gridSpot: getGridSpotKey([1, 2]), + image: 'inventory-mask.png', + }, + + wear_r_ear: { + displayName: 'right earwear', + gridSpot: getGridSpotKey([0, 3]), + image: 'inventory-ears.png', + }, + + wear_l_ear: { + displayName: 'left earwear', + gridSpot: getGridSpotKey([1, 3]), + image: 'inventory-ears.png', + }, + + handcuffs: { + displayName: 'handcuffs', + gridSpot: getGridSpotKey([1, 4]), + hideEmpty: true, + }, + + legcuffs: { + displayName: 'legcuffs', + gridSpot: getGridSpotKey([1, 5]), + hideEmpty: true, + }, + + w_uniform: { + displayName: 'uniform', + gridSpot: getGridSpotKey([2, 1]), + image: 'inventory-uniform.png', + }, + + wear_suit: { + displayName: 'suit', + gridSpot: getGridSpotKey([2, 2]), + image: 'inventory-suit.png', + }, + + gloves: { + displayName: 'gloves', + gridSpot: getGridSpotKey([2, 3]), + image: 'inventory-gloves.png', + }, + + r_hand: { + displayName: 'right hand', + gridSpot: getGridSpotKey([2, 4]), + image: 'inventory-hand_r.png', + additionalComponent: R, + }, + + l_hand: { + displayName: 'left hand', + gridSpot: getGridSpotKey([2, 5]), + image: 'inventory-hand_l.png', + additionalComponent: L, + }, + + shoes: { + displayName: 'shoes', + gridSpot: getGridSpotKey([3, 2]), + image: 'inventory-shoes.png', + }, + + j_store: { + displayName: 'suit storage item', + gridSpot: getGridSpotKey([4, 0]), + image: 'inventory-suit_storage.png', + }, + + id: { + displayName: 'ID', + gridSpot: getGridSpotKey([4, 1]), + image: 'inventory-id.png', + }, + + belt: { + displayName: 'belt', + gridSpot: getGridSpotKey([4, 2]), + image: 'inventory-belt.png', + }, + + back: { + displayName: 'backpack', + gridSpot: getGridSpotKey([4, 3]), + image: 'inventory-back.png', + }, + + l_store: { + displayName: 'left pocket', + gridSpot: getGridSpotKey([4, 4]), + image: 'inventory-pocket.png', + }, + + r_store: { + displayName: 'right pocket', + gridSpot: getGridSpotKey([4, 5]), + image: 'inventory-pocket.png', + }, +}; + +enum ObscuringLevel { + Completely = 1, + Hidden = 2, +} + +type Interactable = { + interacting: BooleanLike; +}; + +/** + * Some possible options: + * + * null - No interactions, no item, but is an available slot + * { interacting: 1 } - No item, but we're interacting with it + * { icon: icon, name: name } - An item with no alternate actions + * that we're not interacting with. + * { icon, name, interacting: 1 } - An item with no alternate actions + * that we're interacting with. + */ +type StripMenuItem = + | null + | Interactable + | (( + | { + icon: string; + name: string; + alternate: string; + } + | { + obscured: ObscuringLevel; + } + | { + no_item_action: string; + } + ) & + Partial); + +type StripMenuData = { + items: Record; + name: string; +}; + +const StripContent = (props: { readonly item: StripMenuItem }) => { + if (props.item && 'name' in props.item) { + return ( + + ); + } + if (props.item && 'obscured' in props.item) { + return ( + + ); + } + return <> ; +}; + +export const StripMenu = (props, context) => { + const { act, data } = useBackend(); + + const gridSpots = new Map(); + for (const key of Object.keys(data.items)) { + const item = data.items[key]; + if (item === null && SLOTS[key].hideEmpty) continue; + gridSpots.set(SLOTS[key].gridSpot, key); + } + + return ( + + + + {range(0, ROWS).map((row) => ( + + + {range(0, COLUMNS).map((column) => { + const key = getGridSpotKey([row, column]); + const keyAtSpot = gridSpots.get(key); + + if (!keyAtSpot) { + return ( + + ); + } + + const item = data.items[keyAtSpot]; + const slot = SLOTS[keyAtSpot]; + + let alternateAction: AlternateAction | undefined; + + let content; + let tooltip; + + if (item === null) { + tooltip = slot.displayName; + } else if ('name' in item) { + alternateAction = ALTERNATE_ACTIONS[item.alternate]; + tooltip = item.name; + } else if ('obscured' in item) { + tooltip = `obscured ${slot.displayName}`; + } else if ('no_item_action' in item) { + tooltip = slot.displayName; + alternateAction = ALTERNATE_ACTIONS[item.no_item_action]; + } + + return ( + + + + + {alternateAction !== undefined && ( + + )} + + + ); + })} + + + ))} + + + + ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/StripMenu.scss b/tgui/packages/tgui/styles/interfaces/StripMenu.scss new file mode 100644 index 0000000000..d2c867c0ac --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/StripMenu.scss @@ -0,0 +1,65 @@ +@use '../base.scss'; + +$background-color: rgba(0, 0, 0, 0.33) !default; + +.StripMenu__cornertext_left { + position: relative; + left: 2px; + text-align: left; + text-shadow: 1px 1px 1px #555; +} + +.StripMenu__cornertext_right { + position: relative; + left: -2px; + text-align: right; + text-shadow: 1px 1px 1px #555; +} + +.StripMenu__iconbox { + height: 100%; + width: 100%; + -ms-interpolation-mode: nearest-neighbor; + vertical-align: middle; +} + +.StripMenu__obscured { + text-align: center; + height: 100%; + width: 100%; +} + +.StripMenu__itembox { + position: relative; + width: 100%; + height: 100%; +} + +.StripMenu__contentbox { + position: relative; +} + +.StripMenu__itembutton { + position: relative; + width: 100%; + height: 100%; + padding: 0; +} + +.StripMenu__itemslot { + position: absolute; + width: 32px; + height: 32px; + left: 50%; + top: 50%; + transform: translateX(-50%) translateY(-50%) scale(0.8); + opacity: 0.7; +} + +.StripMenu__alternativeaction { + background: rgba(0, 0, 0, 0.6); + position: absolute; + bottom: 0; + right: 0; + z-index: 2; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index b51e602746..639365e0e3 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -80,6 +80,7 @@ @include meta.load-css('./interfaces/common/Dpad.scss'); @include meta.load-css('./interfaces/common/ElectricalPanel.scss'); @include meta.load-css('./interfaces/TacticalMap.scss'); +@include meta.load-css('./interfaces/StripMenu.scss'); // Layouts @include meta.load-css('./layouts/Layout.scss');