mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-19 03:55:11 +01:00
RPG Loot: Revisited & READY (#82533)
## About The Pull Request Revival of #72881 A new alt click window with a tarkov-y loading spinner. Replaces the object item window in stat panel. ## Videos <details> <summary>vids</summary> toggleable grouping:  now lists the floor as first obj:  in action:  </details> ## features: - search by name - 515 image generator is much faster than alt click menu - opening a gargantuan amount of items shouldnt freeze your screen - groups similar items together in stacks by default, toggleable - shows tile as first item - <kbd>Shift</kbd> and <kbd>Ctrl</kbd> compatible with LMB 🖱️ - RMB points points at items (sry i could not get MMB working) - key <kbd>Esc</kbd> to exit the window. For devs: - A new image generation tech. - An error refetch mechanic to the Image component - It does not "smart track" the items being added to the pile, just reopen or refresh. This was a design decision. ## Why It's Good For The Game Honestly I just dislike the stat panel Fixes #53824 Fixes  ## Changelog 🆑 add: Added a loot window for alt-clicking tiles. del: Removed the item browser from the stat panel. /🆑 --------- Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> Co-authored-by: AnturK <AnturK@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
This commit is contained in:
@@ -185,6 +185,7 @@
|
||||
#define INIT_ORDER_MINOR_MAPPING -40
|
||||
#define INIT_ORDER_PATH -50
|
||||
#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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+32
-12
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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--
|
||||
@@ -88,12 +88,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
|
||||
|
||||
@@ -166,71 +160,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(
|
||||
@@ -272,16 +201,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
|
||||
|
||||
@@ -301,105 +220,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -540,6 +540,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()
|
||||
@@ -580,8 +582,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
|
||||
@@ -611,6 +611,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
|
||||
QDEL_NULL(view_size)
|
||||
QDEL_NULL(void)
|
||||
QDEL_NULL(tooltips)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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("\
|
||||
<span class='bolddanger'>Your version of Byond doesn't support fast image loading.</span>\n\
|
||||
Detected: [version].[build]\n\
|
||||
Required version for this feature: <b>515.1635</b> or later.\n\
|
||||
Visit <a href=\"https://secure.byond.com/download\">BYOND's website</a> 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)
|
||||
@@ -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)
|
||||
@@ -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--
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
+8
-1
@@ -751,7 +751,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"
|
||||
@@ -3307,6 +3306,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"
|
||||
@@ -4390,6 +4390,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"
|
||||
|
||||
@@ -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<string, string> | 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 <Image fixErrors src={query} {...rest} />;
|
||||
}
|
||||
@@ -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 = <img className={className} src={src} {...computedProps} />;
|
||||
return (
|
||||
<img
|
||||
onError={(event) => {
|
||||
if (fixErrors && attempts.current < maxAttempts) {
|
||||
const imgElement = event.currentTarget;
|
||||
|
||||
if (tooltip) {
|
||||
content = <Tooltip content={tooltip}>{content}</Tooltip>;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
setTimeout(() => {
|
||||
imgElement.src = `${src}?attempt=${attempts.current}`;
|
||||
attempts.current++;
|
||||
}, 1000);
|
||||
}
|
||||
}}
|
||||
src={src}
|
||||
{...computedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string, SearchItem[]> = {};
|
||||
|
||||
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 (
|
||||
<Flex wrap>
|
||||
{filteredContents.map((group) => (
|
||||
<Flex.Item key={group.item.name} m={1}>
|
||||
<LootBox group={group} />
|
||||
</Flex.Item>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -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 = <Icon name="spinner" size={2.4} spin color="gray" />;
|
||||
|
||||
if (!icon) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (icon_state) {
|
||||
return <DmIcon fallback={fallback} icon={icon} icon_state={icon_state} />;
|
||||
}
|
||||
|
||||
return <Image fixErrors src={icon} />;
|
||||
}
|
||||
@@ -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 (
|
||||
<Tooltip content={capitalizeAll(item.name)}>
|
||||
<div
|
||||
className="SearchItem"
|
||||
onClick={(event) =>
|
||||
act('grab', {
|
||||
ctrl: event.ctrlKey,
|
||||
ref: item.ref,
|
||||
shift: event.shiftKey,
|
||||
})
|
||||
}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
act('grab', {
|
||||
middle: true,
|
||||
ref: item.ref,
|
||||
shift: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconDisplay item={item} />
|
||||
{amount > 1 && <div className="SearchItem--amount">{amount}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Flex wrap>
|
||||
{filteredContents.map((item) => (
|
||||
<Flex.Item key={item.ref} m={1}>
|
||||
<LootBox item={item} />
|
||||
</Flex.Item>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -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<Data>();
|
||||
const { contents = [], searching } = data;
|
||||
|
||||
const [grouping, setGrouping] = useState(true);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const total = contents.length ? contents.length - 1 : 0;
|
||||
|
||||
return (
|
||||
<Window height={250} width={190} title={`Contents: ${total}`}>
|
||||
<Window.Content
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KEY.Escape) {
|
||||
Byond.sendMessage('close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Section
|
||||
fill
|
||||
scrollable
|
||||
title={
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
autoFocus
|
||||
fluid
|
||||
onInput={(event, value) => setSearchText(value)}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon={grouping ? 'layer-group' : 'object-ungroup'}
|
||||
selected={grouping}
|
||||
onClick={() => setGrouping(!grouping)}
|
||||
tooltip="Toggle Grouping"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled={!!searching}
|
||||
icon="sync"
|
||||
onClick={() => act('refresh')}
|
||||
tooltip="Refresh"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
{grouping ? (
|
||||
<GroupedContents contents={contents} searchText={searchText} />
|
||||
) : (
|
||||
<RawContents contents={contents} searchText={searchText} />
|
||||
)}
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) => (
|
||||
<Image
|
||||
key={emoji.name}
|
||||
m={0}
|
||||
className={classes(['emojipedia16x16', emoji.name])}
|
||||
tooltip={emoji.name}
|
||||
onClick={() => {
|
||||
copyText(emoji.name);
|
||||
}}
|
||||
/>
|
||||
<Tooltip key={emoji.name} content={emoji.name}>
|
||||
<Image
|
||||
m={0}
|
||||
className={classes(['emojipedia16x16', emoji.name])}
|
||||
onClick={() => {
|
||||
copyText(emoji.name);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Section>
|
||||
</NtosWindow.Content>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user