From 416a5b670e87fd72f25bcd941c5251cd0ce73891 Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+Steals-The-PRs@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:00:01 +0300 Subject: [PATCH] [MIRROR] RPG Loot: Revisited & READY (#2886) * RPG Loot: Revisited & READY * Update client_procs.dm --------- Co-authored-by: NovaBot <154629622+NovaBot13@users.noreply.github.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: SomeRandomOwl <2568378+SomeRandomOwl@users.noreply.github.com> --- code/__DEFINES/subsystems.dm | 1 + code/__DEFINES/traits/declarations.dm | 3 + code/_globalvars/traits/_traits.dm | 1 + code/_onclick/click.dm | 44 +++-- code/_onclick/observer.dm | 2 +- .../subsystem/processing/obj_tab_items.dm | 24 --- code/controllers/subsystem/statpanel.dm | 183 ------------------ code/datums/components/gps.dm | 1 + code/datums/components/rotation.dm | 4 + code/game/objects/items/grenades/_grenade.dm | 1 + .../asset_cache/assets/icon_ref_map.dm | 28 +++ .../asset_cache/transports/asset_transport.dm | 10 +- .../components/binary_devices/passive_gate.dm | 1 + .../binary_devices/pressure_valve.dm | 1 + .../components/binary_devices/pump.dm | 1 + .../binary_devices/temperature_gate.dm | 1 + .../binary_devices/temperature_pump.dm | 1 + .../components/binary_devices/volume_pump.dm | 1 + code/modules/client/client_defines.dm | 5 +- code/modules/client/client_procs.dm | 6 +- code/modules/lootpanel/_lootpanel.dm | 78 ++++++++ code/modules/lootpanel/contents.dm | 49 +++++ code/modules/lootpanel/handlers.dm | 19 ++ code/modules/lootpanel/misc.dm | 48 +++++ code/modules/lootpanel/search_object.dm | 84 ++++++++ code/modules/lootpanel/ss_looting.dm | 39 ++++ code/modules/lootpanel/ui.dm | 46 +++++ .../basic/space_fauna/revenant/_revenant.dm | 2 +- code/modules/tgui/tgui.dm | 2 + code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/lootpanel.dm | 34 ++++ tgstation.dme | 9 +- tgui/packages/tgui/components/DmIcon.tsx | 72 +++++++ tgui/packages/tgui/components/Image.tsx | 45 +++-- tgui/packages/tgui/components/index.ts | 1 + .../interfaces/LootPanel/GroupedContents.tsx | 47 +++++ .../tgui/interfaces/LootPanel/IconDisplay.tsx | 24 +++ .../tgui/interfaces/LootPanel/LootBox.tsx | 53 +++++ .../tgui/interfaces/LootPanel/RawContents.tsx | 28 +++ .../tgui/interfaces/LootPanel/index.tsx | 76 ++++++++ .../tgui/interfaces/LootPanel/types.ts | 13 ++ .../tgui/interfaces/NtosEmojipedia.tsx | 20 +- .../tgui/styles/components/SearchItem.scss | 22 +++ tgui/packages/tgui/styles/main.scss | 1 + 44 files changed, 876 insertions(+), 256 deletions(-) delete mode 100644 code/controllers/subsystem/processing/obj_tab_items.dm create mode 100644 code/modules/asset_cache/assets/icon_ref_map.dm create mode 100644 code/modules/lootpanel/_lootpanel.dm create mode 100644 code/modules/lootpanel/contents.dm create mode 100644 code/modules/lootpanel/handlers.dm create mode 100644 code/modules/lootpanel/misc.dm create mode 100644 code/modules/lootpanel/search_object.dm create mode 100644 code/modules/lootpanel/ss_looting.dm create mode 100644 code/modules/lootpanel/ui.dm create mode 100644 code/modules/unit_tests/lootpanel.dm create mode 100644 tgui/packages/tgui/components/DmIcon.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/index.tsx create mode 100644 tgui/packages/tgui/interfaces/LootPanel/types.ts create mode 100644 tgui/packages/tgui/styles/components/SearchItem.scss diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 6c860e3a272..c4a96d3db97 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -189,6 +189,7 @@ #define INIT_ORDER_PATH -50 #define INIT_ORDER_DECAY -61 //NOVA EDIT ADDITION #define INIT_ORDER_EXPLOSIONS -69 +#define INIT_ORDER_LOOT -70 #define INIT_ORDER_STATPANELS -97 #define INIT_ORDER_BAN_CACHE -98 #define INIT_ORDER_INIT_PROFILER -99 //Near the end, logs the costs of initialize diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 08973d295d9..c57934d2b8a 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1114,4 +1114,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Trait applied to objects and mobs that can attack a boulder and break it down. (See /obj/item/boulder/manual_process()) #define TRAIT_BOULDER_BREAKER "boulder_breaker" +/// Prevents the affected object from opening a loot window via alt click. See atom/AltClick() +#define TRAIT_ALT_CLICK_BLOCKER "no_alt_click" + // END TRAIT DEFINES diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index f8042ae9f64..718f76f9088 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -8,6 +8,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( /atom = list( "TRAIT_AI_PAUSED" = TRAIT_AI_PAUSED, + "TRAIT_ALT_CLICK_BLOCKER" = TRAIT_ALT_CLICK_BLOCKER, "TRAIT_BANNED_FROM_CARGO_SHUTTLE" = TRAIT_BANNED_FROM_CARGO_SHUTTLE, "TRAIT_BEING_SHOCKED" = TRAIT_BEING_SHOCKED, "TRAIT_COMMISSIONED" = TRAIT_COMMISSIONED, diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 3cb12ad8188..2b36dd9ffa0 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -395,14 +395,40 @@ return A.AltClick(src) +/** + * Alt click on an atom. + * Performs alt-click actions before attempting to open a loot window. + * Returns TRUE if successful, FALSE if not. + */ /atom/proc/AltClick(mob/user) if(!user.can_interact_with(src)) return FALSE + if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT) - return - var/turf/T = get_turf(src) - if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T) && !HAS_TRAIT(user, TRAIT_MOVE_VENTCRAWLING)) - user.set_listed_turf(T) + return TRUE + + if(HAS_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER) && !isobserver(user)) + return TRUE + + var/turf/tile = get_turf(src) + if(isnull(tile)) + return FALSE + + if(!isturf(loc) && !isturf(src)) + return FALSE + + if(!user.TurfAdjacent(tile)) + return FALSE + + if(HAS_TRAIT(user, TRAIT_MOVE_VENTCRAWLING)) + return FALSE + + var/datum/lootpanel/panel = user.client?.loot_panel + if(isnull(panel)) + return FALSE + + panel.open(tile) + return TRUE ///The base proc of when something is right clicked on when alt is held - generally use alt_click_secondary instead /atom/proc/alt_click_on_secondary(atom/A) @@ -421,14 +447,8 @@ user.client.toggle_tag_datum(src) return -/// Use this instead of [/mob/proc/AltClickOn] where you only want turf content listing without additional atom alt-click interaction -/atom/proc/AltClickNoInteract(mob/user, atom/A) - var/turf/T = get_turf(A) - if(T && user.TurfAdjacent(T)) - user.set_listed_turf(T) - -/mob/proc/TurfAdjacent(turf/T) - return T.Adjacent(src) +/mob/proc/TurfAdjacent(turf/tile) + return tile.Adjacent(src) /** * Control+Shift click diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm index 1cdb99bbb42..2c236d7232c 100644 --- a/code/_onclick/observer.dm +++ b/code/_onclick/observer.dm @@ -32,7 +32,7 @@ MiddleClickOn(A, params) return if(LAZYACCESS(modifiers, ALT_CLICK)) - AltClickNoInteract(src, A) + A.AltClick(src) return if(LAZYACCESS(modifiers, CTRL_CLICK)) CtrlClickOn(A) diff --git a/code/controllers/subsystem/processing/obj_tab_items.dm b/code/controllers/subsystem/processing/obj_tab_items.dm deleted file mode 100644 index 53786daf011..00000000000 --- a/code/controllers/subsystem/processing/obj_tab_items.dm +++ /dev/null @@ -1,24 +0,0 @@ -PROCESSING_SUBSYSTEM_DEF(obj_tab_items) - name = "Obj Tab Items" - flags = SS_NO_INIT - runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - wait = 0.1 SECONDS - -// I know this is mostly copypasta, but I want to change the processing logic -// Sorry bestie :( -/datum/controller/subsystem/processing/obj_tab_items/fire(resumed = FALSE) - if (!resumed) - currentrun = processing.Copy() - //cache for sanic speed (lists are references anyways) - var/list/current_run = currentrun - - while(current_run.len) - var/datum/thing = current_run[current_run.len] - if(QDELETED(thing)) - processing -= thing - else if(thing.process(wait * 0.1) == PROCESS_KILL) - // fully stop so that a future START_PROCESSING will work - STOP_PROCESSING(src, thing) - if (MC_TICK_CHECK) - return - current_run.len-- diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index d652df3a567..4ec4b90002f 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -107,12 +107,6 @@ SUBSYSTEM_DEF(statpanels) if(update_actions && num_fires % default_wait == 0) set_action_tabs(target, target_mob) - // Handle the examined turf of the stat panel, if it's been long enough, or if we've generated new images for it - var/turf/listed_turf = target_mob?.listed_turf - if(listed_turf && num_fires % default_wait == 0) - if(target.stat_tab == listed_turf.name || !(listed_turf.name in target.panel_tabs)) - set_turf_examine_tab(target, target_mob) - if(MC_TICK_CHECK) return @@ -185,71 +179,6 @@ SUBSYSTEM_DEF(statpanels) target.stat_panel.send_message("update_spells", list(spell_tabs = target.spell_tabs, actions = actions)) -/datum/controller/subsystem/statpanels/proc/set_turf_examine_tab(client/target, mob/target_mob) - var/list/overrides = list() - for(var/image/target_image as anything in target.images) - if(!target_image.loc || target_image.loc.loc != target_mob.listed_turf || !target_image.override) - continue - overrides += target_image.loc - - var/list/atoms_to_display = list(target_mob.listed_turf) - for(var/atom/movable/turf_content as anything in target_mob.listed_turf) - if(turf_content.mouse_opacity == MOUSE_OPACITY_TRANSPARENT) - continue - if(turf_content.invisibility > target_mob.see_invisible) - continue - if(turf_content in overrides) - continue - if(turf_content.IsObscured()) - continue - atoms_to_display += turf_content - - /// Set the atoms we're meant to display - var/datum/object_window_info/obj_window = target.obj_window - obj_window.atoms_to_show = atoms_to_display - START_PROCESSING(SSobj_tab_items, obj_window) - refresh_client_obj_view(target) - -/datum/controller/subsystem/statpanels/proc/refresh_client_obj_view(client/refresh) - var/list/turf_items = return_object_images(refresh) - if(!length(turf_items) || !refresh.mob?.listed_turf) - return - refresh.stat_panel.send_message("update_listedturf", turf_items) - -#define OBJ_IMAGE_LOADING "statpanels obj loading temporary" -/// Returns all our ready object tab images -/// Returns a list in the form list(list(object_name, object_ref, loaded_image), ...) -/datum/controller/subsystem/statpanels/proc/return_object_images(client/load_from) - // You might be inclined to think that this is a waste of cpu time, since we - // A: Double iterate over atoms in the build case, or - // B: Generate these lists over and over in the refresh case - // It's really not very hot. The hot portion of this code is genuinely mostly in the image generation - // So it's ok to pay a performance cost for cleanliness here - - // No turf? go away - if(!load_from.mob?.listed_turf) - return list() - var/datum/object_window_info/obj_window = load_from.obj_window - var/list/already_seen = obj_window.atoms_to_images - var/list/to_make = obj_window.atoms_to_imagify - var/list/turf_items = list() - for(var/atom/turf_item as anything in obj_window.atoms_to_show) - // First, we fill up the list of refs to display - // If we already have one, just use that - var/existing_image = already_seen[turf_item] - if(existing_image == OBJ_IMAGE_LOADING) - continue - // We already have it. Success! - if(existing_image) - turf_items[++turf_items.len] = list("[turf_item.name]", REF(turf_item), existing_image) - continue - // Now, we're gonna queue image generation out of those refs - to_make += turf_item - already_seen[turf_item] = OBJ_IMAGE_LOADING - obj_window.RegisterSignal(turf_item, COMSIG_QDELETING, TYPE_PROC_REF(/datum/object_window_info,viewing_atom_deleted)) // we reset cache if anything in it gets deleted - return turf_items - -#undef OBJ_IMAGE_LOADING /datum/controller/subsystem/statpanels/proc/generate_mc_data() mc_data = list( @@ -291,16 +220,6 @@ SUBSYSTEM_DEF(statpanels) set_action_tabs(target, target_mob) return TRUE - // Handle turfs - - if(target_mob?.listed_turf) - if(!target_mob.TurfAdjacent(target_mob.listed_turf)) - target_mob.set_listed_turf(null) - - else if(target.stat_tab == target_mob?.listed_turf.name || !(target_mob?.listed_turf.name in target.panel_tabs)) - set_turf_examine_tab(target, target_mob) - return TRUE - if(!target.holder) return FALSE @@ -320,105 +239,3 @@ SUBSYSTEM_DEF(statpanels) /// Stat panel window declaration /client/var/datum/tgui_window/stat_panel - -/// Datum that holds and tracks info about a client's object window -/// Really only exists because I want to be able to do logic with signals -/// And need a safe place to do the registration -/datum/object_window_info - /// list of atoms to show to our client via the object tab, at least currently - var/list/atoms_to_show = list() - /// list of atom -> image string for objects we have had in the right click tab - /// this is our caching - var/list/atoms_to_images = list() - /// list of atoms to turn into images for the object tab - var/list/atoms_to_imagify = list() - /// Our owner client - var/client/parent - /// Are we currently tracking a turf? - var/actively_tracking = FALSE - -/datum/object_window_info/New(client/parent) - . = ..() - src.parent = parent - -/datum/object_window_info/Destroy(force) - atoms_to_show = null - atoms_to_images = null - atoms_to_imagify = null - parent.obj_window = null - parent = null - STOP_PROCESSING(SSobj_tab_items, src) - return ..() - -/// Takes a client, attempts to generate object images for it -/// We will update the client with any improvements we make when we're done -/datum/object_window_info/process(seconds_per_tick) - // Cache the datum access for sonic speed - var/list/to_make = atoms_to_imagify - var/list/newly_seen = atoms_to_images - var/index = 0 - for(index in 1 to length(to_make)) - var/atom/thing = to_make[index] - - var/generated_string - if(ismob(thing) || length(thing.overlays) > 2) - generated_string = costly_icon2html(thing, parent, sourceonly=TRUE) - else - generated_string = icon2html(thing, parent, sourceonly=TRUE) - - newly_seen[thing] = generated_string - if(TICK_CHECK) - to_make.Cut(1, index + 1) - index = 0 - break - // If we've not cut yet, do it now - if(index) - to_make.Cut(1, index + 1) - SSstatpanels.refresh_client_obj_view(parent) - if(!length(to_make)) - return PROCESS_KILL - -/datum/object_window_info/proc/start_turf_tracking() - if(actively_tracking) - stop_turf_tracking() - var/static/list/connections = list( - COMSIG_MOVABLE_MOVED = PROC_REF(on_mob_move), - COMSIG_MOB_LOGOUT = PROC_REF(on_mob_logout), - ) - AddComponent(/datum/component/connect_mob_behalf, parent, connections) - actively_tracking = TRUE - -/datum/object_window_info/proc/stop_turf_tracking() - qdel(GetComponent(/datum/component/connect_mob_behalf)) - actively_tracking = FALSE - -/datum/object_window_info/proc/on_mob_move(mob/source) - SIGNAL_HANDLER - var/turf/listed = source.listed_turf - if(!listed || !source.TurfAdjacent(listed)) - source.set_listed_turf(null) - -/datum/object_window_info/proc/on_mob_logout(mob/source) - SIGNAL_HANDLER - on_mob_move(parent.mob) - -/// Clears any cached object window stuff -/// We use hard refs cause we'd need a signal for this anyway. Cleaner this way -/datum/object_window_info/proc/viewing_atom_deleted(atom/deleted) - SIGNAL_HANDLER - atoms_to_show -= deleted - atoms_to_imagify -= deleted - atoms_to_images -= deleted - -/mob/proc/set_listed_turf(turf/new_turf) - listed_turf = new_turf - if(!client) - return - if(!client.obj_window) - client.obj_window = new(client) - if(listed_turf) - client.stat_panel.send_message("create_listedturf", listed_turf.name) - client.obj_window.start_turf_tracking() - else - client.stat_panel.send_message("remove_listedturf") - client.obj_window.stop_turf_tracking() diff --git a/code/datums/components/gps.dm b/code/datums/components/gps.dm index 34aba53e9e4..a84bc1e7595 100644 --- a/code/datums/components/gps.dm +++ b/code/datums/components/gps.dm @@ -89,6 +89,7 @@ GLOBAL_LIST_EMPTY(GPS_list) SIGNAL_HANDLER toggletracking(user) + return COMPONENT_CANCEL_CLICK_ALT ///Toggles the tracking for the gps /datum/component/gps/item/proc/toggletracking(mob/user) diff --git a/code/datums/components/rotation.dm b/code/datums/components/rotation.dm index 7c55579c999..160cc3b7629 100644 --- a/code/datums/components/rotation.dm +++ b/code/datums/components/rotation.dm @@ -26,6 +26,8 @@ RegisterSignal(parent, COMSIG_CLICK_ALT_SECONDARY, PROC_REF(rotate_right)) RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(ExamineMessage)) RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item)) + + ADD_TRAIT(parent, TRAIT_ALT_CLICK_BLOCKER, REF(src)) return ..() /datum/component/simple_rotation/PostTransfer() @@ -41,6 +43,8 @@ COMSIG_ATOM_EXAMINE, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, )) + + REMOVE_TRAIT(parent, TRAIT_ALT_CLICK_BLOCKER, REF(src)) return ..() /datum/component/simple_rotation/Destroy() diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm index 4c737ed53f3..86bb062b651 100644 --- a/code/game/objects/items/grenades/_grenade.dm +++ b/code/game/objects/items/grenades/_grenade.dm @@ -53,6 +53,7 @@ /obj/item/grenade/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT, type) + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) RegisterSignal(src, COMSIG_ITEM_USED_AS_INGREDIENT, PROC_REF(on_used_as_ingredient)) /obj/item/grenade/suicide_act(mob/living/carbon/user) diff --git a/code/modules/asset_cache/assets/icon_ref_map.dm b/code/modules/asset_cache/assets/icon_ref_map.dm new file mode 100644 index 00000000000..2f7f8463099 --- /dev/null +++ b/code/modules/asset_cache/assets/icon_ref_map.dm @@ -0,0 +1,28 @@ +/// Maps icon names to ref values +/datum/asset/json/icon_ref_map + name = "icon_ref_map" + early = TRUE + +/datum/asset/json/icon_ref_map/generate() + var/list/data = list() //"icons/obj/drinks.dmi" => "[0xc000020]" + + //var/start = "0xc000000" + var/value = 0 + + while(TRUE) + value += 1 + var/ref = "\[0xc[num2text(value,6,16)]\]" + var/mystery_meat = locate(ref) + + if(isicon(mystery_meat)) + if(!isfile(mystery_meat)) // Ignore the runtime icons for now + continue + var/path = get_icon_dmi_path(mystery_meat) //Try to get the icon path + if(path) + data[path] = ref + else if(mystery_meat) + continue; //Some other non-icon resource, ogg/json/whatever + else //Out of resources end this, could also try to end this earlier as soon as runtime generated icons appear but eh + break; + + return data diff --git a/code/modules/asset_cache/transports/asset_transport.dm b/code/modules/asset_cache/transports/asset_transport.dm index 19e40fb4884..3dbcc301843 100644 --- a/code/modules/asset_cache/transports/asset_transport.dm +++ b/code/modules/asset_cache/transports/asset_transport.dm @@ -82,11 +82,15 @@ /// asset_list - A list of asset filenames to be sent to the client. Can optionally be assoicated with the asset's asset_cache_item datum. /// Returns TRUE if any assets were sent. /datum/asset_transport/proc/send_assets(client/client, list/asset_list) +#if defined(UNIT_TESTS) + return +#endif + if (!istype(client)) if (ismob(client)) - var/mob/M = client - if (M.client) - client = M.client + var/mob/our_mob = client + if (our_mob.client) + client = our_mob.client else //no stacktrace because this will mainly happen because the client went away return else diff --git a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm index c06863ba092..69b54a01a8c 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm @@ -22,6 +22,7 @@ Passive gate is similar to the regular pump except: /obj/machinery/atmospherics/components/binary/passive_gate/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/passive_gate/add_context(atom/source, list/context, obj/item/held_item, mob/user) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm b/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm index 93192073275..72dd3c7f2f6 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/pressure_valve.dm @@ -14,6 +14,7 @@ /obj/machinery/atmospherics/components/binary/pressure_valve/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/pressure_valve/add_context(atom/source, list/context, obj/item/held_item, mob/user) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm index 3c1ba634cae..50d86992d74 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm @@ -27,6 +27,7 @@ AddComponent(/datum/component/usb_port, list( /obj/item/circuit_component/atmos_pump, )) + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/pump/add_context(atom/source, list/context, obj/item/held_item, mob/user) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm b/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm index bbe788bac53..567759a3829 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/temperature_gate.dm @@ -19,6 +19,7 @@ /obj/machinery/atmospherics/components/binary/temperature_gate/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/temperature_gate/add_context(atom/source, list/context, obj/item/held_item, mob/user) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm index 0fdb5aca6a3..54f277c25b1 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/temperature_pump.dm @@ -14,6 +14,7 @@ /obj/machinery/atmospherics/components/binary/temperature_pump/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/temperature_pump/add_context(atom/source, list/context, obj/item/held_item, mob/user) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm index 139cbe71b36..e190bb47d30 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm @@ -31,6 +31,7 @@ AddComponent(/datum/component/usb_port, list( /obj/item/circuit_component/atmos_volume_pump, )) + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, REF(src)) register_context() /obj/machinery/atmospherics/components/binary/volume_pump/CtrlClick(mob/user) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 45ccda8b92b..3272620a865 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -194,8 +194,6 @@ var/list/spell_tabs = list() ///A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining var/list/recent_examines - ///Our object window datum. It stores info about and handles behavior for the object tab - var/datum/object_window_info/obj_window var/list/parallax_layers var/list/parallax_layers_cached @@ -266,3 +264,6 @@ /// Does this client have typing indicators enabled? var/typing_indicators = FALSE + + /// Loot panel for the client + var/datum/lootpanel/loot_panel diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 6154fcde57f..618d6c8cd1c 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -538,6 +538,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if (!interviewee) initialize_menus() + loot_panel = new(src) + view_size = new(src, getScreenSize(prefs.read_preference(/datum/preference/toggle/widescreen))) view_size.resetFormat() view_size.setZoomMode() @@ -578,8 +580,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( SSserver_maint.UpdateHubStatus() if(credits) QDEL_LIST(credits) - if(obj_window) - QDEL_NULL(obj_window) if(holder) adminGreet(1) holder.owner = null @@ -608,8 +608,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( SSping.currentrun -= src QDEL_NULL(view_size) QDEL_NULL(void) - QDEL_NULL(tooltips) QDEL_NULL(open_loadout_ui) //NOVA EDIT ADDITION + QDEL_NULL(loot_panel) seen_messages = null Master.UpdateTickRate() ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening diff --git a/code/modules/lootpanel/_lootpanel.dm b/code/modules/lootpanel/_lootpanel.dm new file mode 100644 index 00000000000..86a94cc9957 --- /dev/null +++ b/code/modules/lootpanel/_lootpanel.dm @@ -0,0 +1,78 @@ +/** + * ## Loot panel + * A datum that stores info containing the contents of a turf. + * Handles opening the lootpanel UI and searching the turf for items. + */ +/datum/lootpanel + /// The owner of the panel + var/client/owner + /// The list of all search objects indexed. + var/list/datum/search_object/contents = list() + /// The list of search_objects needing processed + var/list/datum/search_object/to_image = list() + /// We've been notified about client version + var/notified = FALSE + /// The turf being searched + var/turf/source_turf + + +/datum/lootpanel/New(client/owner) + . = ..() + + src.owner = owner + + +/datum/lootpanel/Destroy(force) + reset_contents() + owner = null + source_turf = null + + return ..() + + +/datum/lootpanel/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "LootPanel") + ui.set_autoupdate(FALSE) + ui.open() + + +/datum/lootpanel/ui_close(mob/user) + . = ..() + + source_turf = null + reset_contents() + + +/datum/lootpanel/ui_data(mob/user) + var/list/data = list() + + data["contents"] = get_contents() + data["searching"] = length(to_image) + + return data + + +/datum/lootpanel/ui_status(mob/user, datum/ui_state/state) + if(!source_turf.Adjacent(user)) + return UI_CLOSE + + if(user.incapacitated()) + return UI_DISABLED + + return UI_INTERACTIVE + + +/datum/lootpanel/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("grab") + return grab(usr, params) + if("refresh") + return populate_contents() + + return FALSE diff --git a/code/modules/lootpanel/contents.dm b/code/modules/lootpanel/contents.dm new file mode 100644 index 00000000000..4bb255b1561 --- /dev/null +++ b/code/modules/lootpanel/contents.dm @@ -0,0 +1,49 @@ +/// Adds the item to contents and to_image (if needed) +/datum/lootpanel/proc/add_to_index(datum/search_object/index) + RegisterSignal(index, COMSIG_QDELETING, PROC_REF(on_searchable_deleted)) + if(isnull(index.icon)) + to_image += index + + contents += index + + +/// Used to populate contents and start generating if needed +/datum/lootpanel/proc/populate_contents() + if(length(contents)) + reset_contents() + + // Add source turf first + var/datum/search_object/source = new(owner, source_turf) + add_to_index(source) + + for(var/atom/thing as anything in source_turf.contents) + // validate + if(thing.mouse_opacity == MOUSE_OPACITY_TRANSPARENT) + continue + if(thing.IsObscured()) + continue + if(thing.invisibility > owner.mob.see_invisible) + continue + + // convert + var/datum/search_object/index = new(owner, thing) + add_to_index(index) + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) + window?.send_update() + + if(length(to_image)) + SSlooting.backlog += src + + +/// For: Resetting to empty. Ignores the searchable qdel event +/datum/lootpanel/proc/reset_contents() + for(var/datum/search_object/index as anything in contents) + contents -= index + to_image -= index + + if(QDELETED(index)) + continue + + UnregisterSignal(index, COMSIG_QDELETING) + qdel(index) diff --git a/code/modules/lootpanel/handlers.dm b/code/modules/lootpanel/handlers.dm new file mode 100644 index 00000000000..40a76974ed4 --- /dev/null +++ b/code/modules/lootpanel/handlers.dm @@ -0,0 +1,19 @@ +/// On contents change, either reset or update +/datum/lootpanel/proc/on_searchable_deleted(datum/search_object/source) + SIGNAL_HANDLER + + contents -= source + to_image -= source + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) +#if !defined(UNIT_TESTS) // we dont want to delete contents if we're testing + if(isnull(window)) + reset_contents() + return +#endif + + if(isturf(source.item)) + populate_contents() + return + + window?.send_update() diff --git a/code/modules/lootpanel/misc.dm b/code/modules/lootpanel/misc.dm new file mode 100644 index 00000000000..6e8448b8205 --- /dev/null +++ b/code/modules/lootpanel/misc.dm @@ -0,0 +1,48 @@ +/// Helper to open the panel +/datum/lootpanel/proc/open(turf/tile) + source_turf = tile + +#if !defined(OPENDREAM) && !defined(UNIT_TESTS) + if(!notified) + var/build = owner.byond_build + var/version = owner.byond_version + if(build < 515 || (build == 515 && version < 1635)) + to_chat(owner.mob, examine_block(span_info("\ + Your version of Byond doesn't support fast image loading.\n\ + Detected: [version].[build]\n\ + Required version for this feature: 515.1635 or later.\n\ + Visit BYOND's website to get the latest version of BYOND.\n\ + "))) + + notified = TRUE +#endif + + populate_contents() + ui_interact(owner.mob) + + +/** + * Called by SSlooting whenever this datum is added to its backlog. + * Iterates over to_image list to create icons, then removes them. + * Returns boolean - whether this proc has finished the queue or not. + */ +/datum/lootpanel/proc/process_images() + for(var/datum/search_object/index as anything in to_image) + to_image -= index + + if(QDELETED(index) || index.icon) + continue + + index.generate_icon(owner) + + if(TICK_CHECK) + break + + var/datum/tgui/window = SStgui.get_open_ui(owner.mob, src) + if(isnull(window)) + reset_contents() + return TRUE + + window.send_update() + + return !length(to_image) diff --git a/code/modules/lootpanel/search_object.dm b/code/modules/lootpanel/search_object.dm new file mode 100644 index 00000000000..520228e465e --- /dev/null +++ b/code/modules/lootpanel/search_object.dm @@ -0,0 +1,84 @@ +/** + * ## Search Object + * An object for content lists. Compacted item data. + */ +/datum/search_object + /// Item we're indexing + var/atom/item + /// Url to the image of the object + var/icon + /// Icon state, for inexpensive icons + var/icon_state + /// Name of the original object + var/name + /// Typepath of the original object for ui grouping + var/path + + +/datum/search_object/New(client/owner, atom/item) + . = ..() + + src.item = item + name = item.name + if(isobj(item)) + path = item.type + + if(isturf(item)) + RegisterSignal(item, COMSIG_TURF_CHANGE, PROC_REF(on_turf_change)) + else + RegisterSignals(item, list( + COMSIG_ITEM_PICKUP, + COMSIG_MOVABLE_MOVED, + COMSIG_QDELETING, + ), PROC_REF(on_item_moved)) + + // Icon generation conditions ////////////// + // Condition 1: Icon is complex + if(ismob(item) || length(item.overlays) > 2) + return + + // Condition 2: Can't get icon path + if(!isfile(item.icon) || !length("[item.icon]")) + return + + // Condition 3: Using opendream +#if defined(OPENDREAM) || defined(UNIT_TESTS) + return +#endif + + // Condition 4: Using older byond version + var/build = owner.byond_build + var/version = owner.byond_version + if(build < 515 || (build == 515 && version < 1635)) + return + + icon = "[item.icon]" + icon_state = item.icon_state + + +/datum/search_object/Destroy(force) + item = null + + return ..() + + +/// Generates the icon for the search object. This is the expensive part. +/datum/search_object/proc/generate_icon(client/owner) + if(ismob(item) || length(item.overlays) > 2) + icon = costly_icon2html(item, owner, sourceonly = TRUE) + else // our pre 515.1635 fallback for normal items + icon = icon2html(item, owner, sourceonly = TRUE) + + +/// Parent item has been altered, search object no longer valid +/datum/search_object/proc/on_item_moved(atom/source) + SIGNAL_HANDLER + + qdel(src) + + +/// Parent tile has been altered, entire search needs reset +/datum/search_object/proc/on_turf_change(turf/source, path, list/new_baseturfs, flags, list/post_change_callbacks) + SIGNAL_HANDLER + + post_change_callbacks += CALLBACK(src, GLOBAL_PROC_REF(qdel), src) diff --git a/code/modules/lootpanel/ss_looting.dm b/code/modules/lootpanel/ss_looting.dm new file mode 100644 index 00000000000..cf0882fa890 --- /dev/null +++ b/code/modules/lootpanel/ss_looting.dm @@ -0,0 +1,39 @@ + +/// Queues image generation for search objects without icons +SUBSYSTEM_DEF(looting) + name = "Loot Icon Generation" + init_order = INIT_ORDER_LOOT + priority = FIRE_PRIORITY_PROCESS + wait = 0.5 SECONDS + /// Backlog of items. Gets put into processing + var/list/datum/lootpanel/backlog = list() + /// Actively processing items + var/list/datum/lootpanel/processing = list() + + +/datum/controller/subsystem/looting/stat_entry(msg) + msg = "P:[length(backlog)]" + return ..() + + +/datum/controller/subsystem/looting/fire(resumed) + if(!length(backlog)) + return + + if(!resumed) + processing = backlog + backlog = list() + + while(length(processing)) + var/datum/lootpanel/panel = processing[length(processing)] + if(QDELETED(panel) || !length(panel.to_image)) + processing.len-- + continue + + if(!panel.process_images()) + backlog += panel + + if(MC_TICK_CHECK) + return + + processing.len-- diff --git a/code/modules/lootpanel/ui.dm b/code/modules/lootpanel/ui.dm new file mode 100644 index 00000000000..3c829871c60 --- /dev/null +++ b/code/modules/lootpanel/ui.dm @@ -0,0 +1,46 @@ +/// UI helper for converting the associative list to a list of lists +/datum/lootpanel/proc/get_contents() + var/list/items = list() + + for(var/datum/search_object/index as anything in contents) + UNTYPED_LIST_ADD(items, list( + "icon_state" = index.icon_state, + "icon" = index.icon, + "name" = index.name, + "path" = index.path, + "ref" = REF(index), + )) + + return items + + +/// Clicks an object from the contents. Validates the object and the user +/datum/lootpanel/proc/grab(mob/user, list/params) + var/ref = params["ref"] + if(isnull(ref)) + return FALSE + + if(!source_turf.Adjacent(user)) // Source tile is no longer valid + reset_contents() + return FALSE + + var/datum/search_object/index = locate(ref) in contents + var/atom/thing = index?.item + if(QDELETED(index) || QDELETED(thing)) // Obj is gone + return FALSE + + if(thing != source_turf && !(locate(thing) in source_turf.contents)) + qdel(index) // Item has moved + return TRUE + + var/modifiers = "" + if(params["ctrl"]) + modifiers += "ctrl=1;" + if(params["middle"]) + modifiers += "middle=1;" + if(params["shift"]) + modifiers += "shift=1;" + + user.ClickOn(thing, modifiers) + + return TRUE diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm index 266e7b5b414..c7ceb73e3c8 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm @@ -204,7 +204,7 @@ ShiftClickOn(A) return if(LAZYACCESS(modifiers, ALT_CLICK)) - AltClickNoInteract(src, A) + A.AltClick(src) return if(LAZYACCESS(modifiers, RIGHT_CLICK)) ranged_secondary_attack(A, modifiers) diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 64bd496f2bb..754335494f9 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -118,6 +118,8 @@ /datum/asset/simple/namespaced/fontawesome)) flush_queue |= window.send_asset(get_asset_datum( /datum/asset/simple/namespaced/tgfont)) + flush_queue |= window.send_asset(get_asset_datum( + /datum/asset/json/icon_ref_map)) for(var/datum/asset/asset in src_object.ui_assets(user)) flush_queue |= window.send_asset(asset) if (flush_queue) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 892540192b3..a82e51bc737 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -168,6 +168,7 @@ #include "ling_decap.dm" #include "liver.dm" #include "load_map_security.dm" +#include "lootpanel.dm" #include "lungs.dm" #include "machine_disassembly.dm" #include "mafia.dm" diff --git a/code/modules/unit_tests/lootpanel.dm b/code/modules/unit_tests/lootpanel.dm new file mode 100644 index 00000000000..c0bec13288c --- /dev/null +++ b/code/modules/unit_tests/lootpanel.dm @@ -0,0 +1,34 @@ +/datum/unit_test/lootpanel + abstract_type = /datum/unit_test/lootpanel + +/datum/unit_test/lootpanel/contents/Run() + var/datum/client_interface/mock_client = new() + var/datum/lootpanel/panel = new(mock_client) + var/mob/living/carbon/human/labrat = allocate(/mob/living/carbon/human/consistent) + mock_client.mob = labrat + var/turf/one_over = locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z) + var/obj/item/storage/toolbox/box = allocate(/obj/item/storage/toolbox, one_over) + + panel.open(one_over) + TEST_ASSERT_EQUAL(length(panel.contents), 2, "Contents should populate on open") + TEST_ASSERT_EQUAL(length(panel.to_image), 2, "to_image should've populated (unit testing)") + TEST_ASSERT_EQUAL(panel.contents[1].item, one_over, "First item should be the source turf") + + var/datum/search_object/searchable = panel.contents[2] + TEST_ASSERT_EQUAL(searchable.item, box, "Second item should be the box") + + qdel(box) + TEST_ASSERT_EQUAL(length(panel.contents), 1, "Contents should update on searchobj deleted") + TEST_ASSERT_EQUAL(length(panel.to_image), 1, "to_image should update on searchobj deleted") + + var/obj/item/storage/toolbox/new_box = allocate(/obj/item/storage/toolbox, one_over) + TEST_ASSERT_EQUAL(length(panel.contents), 1, "Contents shouldn't update, we're dumb") + TEST_ASSERT_EQUAL(length(panel.to_image), 1, "to_image shouldn't update, we're dumb") + + panel.populate_contents() // this also calls reset_contents bc length(contents) + TEST_ASSERT_EQUAL(length(panel.contents), 2, "Contents should repopulate with the new toolbox") + + panel.populate_contents() + TEST_ASSERT_EQUAL(length(panel.contents), 2, "Panel shouldnt dupe searchables if reopened") + + mock_client.mob = null diff --git a/tgstation.dme b/tgstation.dme index a38cf3b9b2b..3e9e29ccef2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -853,7 +853,6 @@ #include "code\controllers\subsystem\processing\greyscale.dm" #include "code\controllers\subsystem\processing\instruments.dm" #include "code\controllers\subsystem\processing\obj.dm" -#include "code\controllers\subsystem\processing\obj_tab_items.dm" #include "code\controllers\subsystem\processing\plumbing.dm" #include "code\controllers\subsystem\processing\processing.dm" #include "code\controllers\subsystem\processing\projectiles.dm" @@ -3408,6 +3407,7 @@ #include "code\modules\asset_cache\assets\fontawesome.dm" #include "code\modules\asset_cache\assets\genetics.dm" #include "code\modules\asset_cache\assets\headers.dm" +#include "code\modules\asset_cache\assets\icon_ref_map.dm" #include "code\modules\asset_cache\assets\inventory.dm" #include "code\modules\asset_cache\assets\irv.dm" #include "code\modules\asset_cache\assets\jquery.dm" @@ -4490,6 +4490,13 @@ #include "code\modules\logging\categories\log_category_silo.dm" #include "code\modules\logging\categories\log_category_target_zone_switch.dm" #include "code\modules\logging\categories\log_category_uplink.dm" +#include "code\modules\lootpanel\_lootpanel.dm" +#include "code\modules\lootpanel\contents.dm" +#include "code\modules\lootpanel\handlers.dm" +#include "code\modules\lootpanel\misc.dm" +#include "code\modules\lootpanel\search_object.dm" +#include "code\modules\lootpanel\ss_looting.dm" +#include "code\modules\lootpanel\ui.dm" #include "code\modules\mafia\_defines.dm" #include "code\modules\mafia\controller.dm" #include "code\modules\mafia\controller_ui.dm" diff --git a/tgui/packages/tgui/components/DmIcon.tsx b/tgui/packages/tgui/components/DmIcon.tsx new file mode 100644 index 00000000000..fb6816576ac --- /dev/null +++ b/tgui/packages/tgui/components/DmIcon.tsx @@ -0,0 +1,72 @@ +import { ReactNode, useEffect, useState } from 'react'; + +import { resolveAsset } from '../assets'; +import { fetchRetry } from '../http'; +import { BoxProps } from './Box'; +import { Image } from './Image'; + +enum Direction { + NORTH = 1, + SOUTH = 2, + EAST = 4, + WEST = 8, + NORTHEAST = NORTH | EAST, + NORTHWEST = NORTH | WEST, + SOUTHEAST = SOUTH | EAST, + SOUTHWEST = SOUTH | WEST, +} + +type Props = { + /** Required: The path of the icon */ + icon: string; + /** Required: The state of the icon */ + icon_state: string; +} & Partial<{ + /** Facing direction. See direction enum. Default is South */ + direction: Direction; + /** Fallback icon. */ + fallback: ReactNode; + /** Frame number. Default is 1 */ + frame: number; + /** Movement state. Default is false */ + movement: boolean; +}> & + BoxProps; + +let refMap: Record | undefined; + +export function DmIcon(props: Props) { + const { + className, + direction = Direction.SOUTH, + fallback, + frame = 1, + icon_state, + icon, + movement = false, + ...rest + } = props; + + const [iconRef, setIconRef] = useState(''); + + const query = `${iconRef}?state=${icon_state}&dir=${direction}&movement=${movement}&frame=${frame}`; + + useEffect(() => { + async function fetchRefMap() { + const response = await fetchRetry(resolveAsset('icon_ref_map.json')); + const data = await response.json(); + refMap = data; + setIconRef(data[icon]); + } + + if (!refMap) { + fetchRefMap(); + } else { + setIconRef(refMap[icon]); + } + }, []); + + if (!iconRef) return fallback; + + return ; +} diff --git a/tgui/packages/tgui/components/Image.tsx b/tgui/packages/tgui/components/Image.tsx index 3e1519bfbbf..5d3a943feb0 100644 --- a/tgui/packages/tgui/components/Image.tsx +++ b/tgui/packages/tgui/components/Image.tsx @@ -1,12 +1,14 @@ -import { ReactNode } from 'react'; +import { useRef } from 'react'; import { BoxProps, computeBoxProps } from './Box'; -import { Tooltip } from './Tooltip'; type Props = Partial<{ - fixBlur: boolean; // true is default, this is an ie thing - objectFit: 'contain' | 'cover'; // fill is default - tooltip: ReactNode; + /** True is default, this fixes an ie thing */ + fixBlur: boolean; + /** False by default. Good if you're fetching images on UIs that do not auto update. This will attempt to fix the 'x' icon 5 times. */ + fixErrors: boolean; + /** Fill is default. */ + objectFit: 'contain' | 'cover'; }> & IconUnion & BoxProps; @@ -22,16 +24,18 @@ type IconUnion = src?: string; }; +const maxAttempts = 5; + /** Image component. Use this instead of Box as="img". */ -export const Image = (props: Props) => { +export function Image(props: Props) { const { - className, fixBlur = true, + fixErrors = false, objectFit = 'fill', src, - tooltip, ...rest } = props; + const attempts = useRef(0); const computedProps = computeBoxProps(rest); computedProps['style'] = { @@ -40,11 +44,20 @@ export const Image = (props: Props) => { objectFit, }; - let content = ; - - if (tooltip) { - content = {content}; - } - - return content; -}; + return ( + { + if (fixErrors && attempts.current < maxAttempts) { + const imgElement = event.currentTarget; + + setTimeout(() => { + imgElement.src = `${src}?attempt=${attempts.current}`; + attempts.current++; + }, 1000); + } + }} + src={src} + {...computedProps} + /> + ); +} diff --git a/tgui/packages/tgui/components/index.ts b/tgui/packages/tgui/components/index.ts index 1a5f477d256..fdeb475ed3b 100644 --- a/tgui/packages/tgui/components/index.ts +++ b/tgui/packages/tgui/components/index.ts @@ -17,6 +17,7 @@ export { ColorBox } from './ColorBox'; export { Dialog } from './Dialog'; export { Dimmer } from './Dimmer'; export { Divider } from './Divider'; +export { DmIcon } from './DmIcon'; export { DraggableControl } from './DraggableControl'; export { Dropdown } from './Dropdown'; export { FitText } from './FitText'; diff --git a/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx b/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx new file mode 100644 index 00000000000..5bfecce0b8a --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx @@ -0,0 +1,47 @@ +import { createSearch } from 'common/string'; +import { useMemo } from 'react'; + +import { Flex } from '../../components'; +import { LootBox } from './LootBox'; +import { SearchGroup, SearchItem } from './types'; + +type Props = { + contents: SearchItem[]; + searchText: string; +}; + +export function GroupedContents(props: Props) { + const { contents, searchText } = props; + + // limitations: items with different stack counts, charges etc. + const contentsByPath = useMemo(() => { + const acc: Record = {}; + + for (let i = 0; i < contents.length; i++) { + const item = contents[i]; + if (item.path) { + if (!acc[item.path]) { + acc[item.path] = []; + } + acc[item.path].push(item); + } else { + acc[item.ref] = [item]; + } + } + return acc; + }, [contents]); + + const filteredContents: SearchGroup[] = Object.entries(contentsByPath) + .filter(createSearch(searchText, ([_, items]) => items[0].name)) + .map(([_, items]) => ({ amount: items.length, item: items[0] })); + + return ( + + {filteredContents.map((group) => ( + + + + ))} + + ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx b/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx new file mode 100644 index 00000000000..889d50de884 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx @@ -0,0 +1,24 @@ +import { DmIcon, Icon, Image } from '../../components'; +import { SearchItem } from './types'; + +type Props = { + item: SearchItem; +}; + +export function IconDisplay(props: Props) { + const { + item: { icon, icon_state }, + } = props; + + const fallback = ; + + if (!icon) { + return fallback; + } + + if (icon_state) { + return ; + } + + return ; +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx b/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx new file mode 100644 index 00000000000..22b0c8532dd --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx @@ -0,0 +1,53 @@ +import { capitalizeAll } from 'common/string'; + +import { useBackend } from '../../backend'; +import { Tooltip } from '../../components'; +import { IconDisplay } from './IconDisplay'; +import { SearchGroup, SearchItem } from './types'; + +type Props = + | { + item: SearchItem; + } + | { + group: SearchGroup; + }; + +export function LootBox(props: Props) { + const { act } = useBackend(); + + let amount = 0; + let item: SearchItem; + if ('group' in props) { + amount = props.group.amount; + item = props.group.item; + } else { + item = props.item; + } + + return ( + +
+ act('grab', { + ctrl: event.ctrlKey, + ref: item.ref, + shift: event.shiftKey, + }) + } + onContextMenu={(event) => { + event.preventDefault(); + act('grab', { + middle: true, + ref: item.ref, + shift: true, + }); + }} + > + + {amount > 1 &&
{amount}
} +
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx b/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx new file mode 100644 index 00000000000..4241d36dc0a --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx @@ -0,0 +1,28 @@ +import { createSearch } from 'common/string'; + +import { Flex } from '../../components'; +import { LootBox } from './LootBox'; +import { SearchItem } from './types'; + +type Props = { + contents: SearchItem[]; + searchText: string; +}; + +export function RawContents(props: Props) { + const { contents, searchText } = props; + + const filteredContents = contents.filter( + createSearch(searchText, (item: SearchItem) => item.name), + ); + + return ( + + {filteredContents.map((item) => ( + + + + ))} + + ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/index.tsx b/tgui/packages/tgui/interfaces/LootPanel/index.tsx new file mode 100644 index 00000000000..bd64113f302 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/index.tsx @@ -0,0 +1,76 @@ +import { KEY } from 'common/keys'; +import { BooleanLike } from 'common/react'; +import { useState } from 'react'; + +import { useBackend } from '../../backend'; +import { Button, Input, Section, Stack } from '../../components'; +import { Window } from '../../layouts'; +import { GroupedContents } from './GroupedContents'; +import { RawContents } from './RawContents'; +import { SearchItem } from './types'; + +type Data = { + contents: SearchItem[]; + searching: BooleanLike; +}; + +export function LootPanel(props) { + const { act, data } = useBackend(); + const { contents = [], searching } = data; + + const [grouping, setGrouping] = useState(true); + const [searchText, setSearchText] = useState(''); + + const total = contents.length ? contents.length - 1 : 0; + + return ( + + { + if (event.key === KEY.Escape) { + Byond.sendMessage('close'); + } + }} + > +
+ + setSearchText(value)} + placeholder="Search" + /> + + +
+
+
+ ); +} diff --git a/tgui/packages/tgui/interfaces/LootPanel/types.ts b/tgui/packages/tgui/interfaces/LootPanel/types.ts new file mode 100644 index 00000000000..f17b02b0c13 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LootPanel/types.ts @@ -0,0 +1,13 @@ +export type SearchItem = { + name: string; + path: string; + ref: string; +} & Partial<{ + icon: string; + icon_state: string; +}>; + +export type SearchGroup = { + amount: number; + item: SearchItem; +}; diff --git a/tgui/packages/tgui/interfaces/NtosEmojipedia.tsx b/tgui/packages/tgui/interfaces/NtosEmojipedia.tsx index 3852e90ac26..d9e6c7233ee 100644 --- a/tgui/packages/tgui/interfaces/NtosEmojipedia.tsx +++ b/tgui/packages/tgui/interfaces/NtosEmojipedia.tsx @@ -3,7 +3,7 @@ import { createSearch } from 'common/string'; import { useState } from 'react'; import { useBackend } from '../backend'; -import { Button, Image, Input, Section } from '../components'; +import { Button, Image, Input, Section, Tooltip } from '../components'; import { NtosWindow } from '../layouts'; type Data = { @@ -44,15 +44,15 @@ export const NtosEmojipedia = (props) => { } > {filteredEmojis.map((emoji) => ( - { - copyText(emoji.name); - }} - /> + + { + copyText(emoji.name); + }} + /> + ))} diff --git a/tgui/packages/tgui/styles/components/SearchItem.scss b/tgui/packages/tgui/styles/components/SearchItem.scss new file mode 100644 index 00000000000..76946ec9c5a --- /dev/null +++ b/tgui/packages/tgui/styles/components/SearchItem.scss @@ -0,0 +1,22 @@ +@use '../colors.scss'; + +.SearchItem { + align-items: center; + background: black; + border: thin solid #212121; + display: flex; + height: 3rem; + justify-content: center; + position: relative; + width: 3rem; + margin-bottom: 0; +} + +.SearchItem--amount { + bottom: -1rem; + color: colors.$teal; + font-family: 'Roboto', sans-serif; + font-size: 1.5rem; + position: absolute; + right: -4px; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 25443ae606c..87715e2ae30 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -38,6 +38,7 @@ @include meta.load-css('./components/NumberInput.scss'); @include meta.load-css('./components/ProgressBar.scss'); @include meta.load-css('./components/RoundGauge.scss'); +@include meta.load-css('./components/SearchItem.scss'); @include meta.load-css('./components/Section.scss'); @include meta.load-css('./components/Slider.scss'); @include meta.load-css('./components/Stack.scss');