mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 10:43:20 +00:00
[MIRROR] Replace the alt click menu with the RPG Lootpanel (#11170)
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com>
This commit is contained in:
committed by
GitHub
parent
5a7afc12df
commit
23fee17c6d
@@ -164,6 +164,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
|
||||
#define INIT_ORDER_MAPRENAME -60 //Initiating after Ticker to ensure everything is loaded and everything we rely on us working
|
||||
#define INIT_ORDER_WIKI -61
|
||||
#define INIT_ORDER_ATC -70
|
||||
#define INIT_ORDER_LOOT -80
|
||||
#define INIT_ORDER_STATPANELS -98
|
||||
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
|
||||
|
||||
|
||||
@@ -12,3 +12,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
|
||||
|
||||
/// Climbable trait, given and taken by the climbable element when added or removed. Exists to be easily checked via HAS_TRAIT().
|
||||
#define TRAIT_CLIMBABLE "trait_climbable"
|
||||
|
||||
/// Prevents the affected object from opening a loot window via alt click. See atom/AltClick()
|
||||
#define TRAIT_ALT_CLICK_BLOCKER "no_alt_click"
|
||||
|
||||
@@ -17,6 +17,9 @@ GLOBAL_LIST_INIT(radiation_levels, list(
|
||||
|
||||
/* CHOMPRemove, see traits.dm
|
||||
GLOBAL_LIST_INIT(traits_by_type, list(
|
||||
/atom = list(
|
||||
"TRAIT_ALT_CLICK_BLOCKER" = TRAIT_ALT_CLICK_BLOCKER,
|
||||
),
|
||||
/mob = list(
|
||||
"TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER,
|
||||
),
|
||||
|
||||
@@ -293,11 +293,37 @@
|
||||
A.AltClick(src)
|
||||
return
|
||||
|
||||
/**
|
||||
* 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(var/mob/user)
|
||||
var/turf/T = get_turf(src)
|
||||
if(T && user.TurfAdjacent(T))
|
||||
user.set_listed_turf(T)
|
||||
return 1
|
||||
// if(!user.can_interact_with(src))
|
||||
// return FALSE
|
||||
|
||||
// if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT)
|
||||
// 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
|
||||
|
||||
var/datum/lootpanel/panel = user.client?.loot_panel
|
||||
if(isnull(panel))
|
||||
return FALSE
|
||||
|
||||
panel.open(tile)
|
||||
return TRUE
|
||||
|
||||
/mob/proc/TurfAdjacent(var/turf/T)
|
||||
return T.AdjacentQuick(src)
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
if(modifiers["alt"]) // alt and alt-gr (rightalt)
|
||||
var/turf/T = get_turf(A)
|
||||
if(T && TurfAdjacent(T))
|
||||
set_listed_turf(T)
|
||||
T.AltClick(src)
|
||||
return
|
||||
// You are responsible for checking config.ghost_interaction when you override this function
|
||||
// Not all of them require checking, see below
|
||||
|
||||
@@ -9,7 +9,6 @@ SUBSYSTEM_DEF(statpanels)
|
||||
var/list/currentrun = list()
|
||||
var/list/global_data
|
||||
var/list/mc_data
|
||||
var/list/cached_images = list()
|
||||
|
||||
///how many subsystem fires between most tab updates
|
||||
var/default_wait = 10
|
||||
@@ -102,12 +101,6 @@ SUBSYSTEM_DEF(statpanels)
|
||||
if((num_fires % misc_wait == 0))
|
||||
update_misc_tabs(target,target_mob)
|
||||
|
||||
var/datum/object_window_info/obj_window = target.obj_window
|
||||
if(obj_window)
|
||||
if(obj_window.flags & TURFLIST_UPDATE_QUEUED)
|
||||
immediate_send_stat_data(target)
|
||||
obj_window.flags = 0
|
||||
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
|
||||
@@ -153,14 +146,17 @@ SUBSYSTEM_DEF(statpanels)
|
||||
var/description_holders = target.description_holders
|
||||
var/list/examine_update = list()
|
||||
|
||||
if(!target.obj_window)
|
||||
target.obj_window = new(target)
|
||||
if(!target.examine_icon && !target.obj_window.examine_target && target.stat_tab == "Examine")
|
||||
target.obj_window.examine_target = description_holders["icon"]
|
||||
target.obj_window.atoms_to_show += target.obj_window.examine_target
|
||||
START_PROCESSING(SSobj_tab_items, target.obj_window)
|
||||
refresh_client_obj_view(target)
|
||||
examine_update += "[target.examine_icon] " + span_giant("[description_holders["name"]]") //The name, written in big letters.
|
||||
var/atom/atom_icon = description_holders["icon"]
|
||||
var/shown_icon = target.examine_icon
|
||||
if(!shown_icon)
|
||||
if(ismob(atom_icon) || length(atom_icon.overlays) > 0)
|
||||
var/force_south = FALSE
|
||||
if(isliving(atom_icon))
|
||||
force_south = TRUE
|
||||
shown_icon = costly_icon2html(atom_icon, target, sourceonly=TRUE, force_south = force_south)
|
||||
else
|
||||
shown_icon = icon2html(atom_icon, target, sourceonly=TRUE)
|
||||
examine_update += "<img src=\"[shown_icon]\" /> " + span_giant("[description_holders["name"]]") //The name, written in big letters.
|
||||
examine_update += "[description_holders["desc"]]" //the default examine text.
|
||||
if(description_holders["info"])
|
||||
examine_update += span_blue(span_bold("[replacetext(description_holders["info"], "\n", "<BR>")]")) + "<br />" //Blue, informative text.
|
||||
@@ -205,81 +201,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)
|
||||
if(!target)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data()
|
||||
return
|
||||
var/list/overrides = list()
|
||||
for(var/image/target_image as anything in target.images)
|
||||
if(!target_image.loc || target_image.loc.loc != target.tracked_turf || !target_image.override)
|
||||
continue
|
||||
overrides += target_image.loc
|
||||
|
||||
var/list/atoms_to_display = list(target.tracked_turf)
|
||||
for(var/atom/movable/turf_content as anything in target.tracked_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
|
||||
if(!obj_window)
|
||||
return // previous one no longer exists
|
||||
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)
|
||||
if(turf_item == obj_window.examine_target) //not actually a turf item get trolled
|
||||
load_from.examine_icon = "<img src=\"[existing_image]\" />"
|
||||
obj_window.examine_target = null
|
||||
set_examine_tab(load_from)
|
||||
continue
|
||||
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_PARENT_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(
|
||||
list("CPU:", world.cpu),
|
||||
@@ -324,16 +245,6 @@ SUBSYSTEM_DEF(statpanels)
|
||||
set_action_tabs(target, target_mob)
|
||||
return TRUE
|
||||
|
||||
// Handle turfs
|
||||
|
||||
if(target.tracked_turf)
|
||||
if(!target_mob.TurfAdjacent(target.tracked_turf))
|
||||
target_mob.set_listed_turf(null)
|
||||
|
||||
else if(target.stat_tab == target.tracked_turf.name || !(target.tracked_turf.name in target.panel_tabs))
|
||||
set_turf_examine_tab(target, target_mob)
|
||||
return TRUE
|
||||
|
||||
if(!target.holder)
|
||||
return FALSE
|
||||
|
||||
@@ -351,136 +262,5 @@ SUBSYSTEM_DEF(statpanels)
|
||||
else if(length(GLOB.sdql2_queries) && target.stat_tab == "SDQL2")
|
||||
set_SDQL2_tab(target)
|
||||
|
||||
/atom/proc/remove_from_cache()
|
||||
SIGNAL_HANDLER
|
||||
SSstatpanels.cached_images -= REF(src)
|
||||
|
||||
/// Stat panel window declaration
|
||||
/client/var/datum/tgui_window/stat_panel
|
||||
/// Turf examine turf
|
||||
/client/var/turf/tracked_turf
|
||||
|
||||
/// 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
|
||||
///For reusing this logic for examines
|
||||
var/atom/examine_target
|
||||
var/flags = 0
|
||||
|
||||
/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]
|
||||
|
||||
if(!thing) // A null thing snuck in somehow
|
||||
continue
|
||||
|
||||
var/generated_string
|
||||
if(ismob(thing) || length(thing.overlays) > 0)
|
||||
var/force_south = FALSE
|
||||
if(isliving(thing))
|
||||
force_south = TRUE
|
||||
generated_string = costly_icon2html(thing, parent, sourceonly=TRUE, force_south = force_south)
|
||||
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(turf/new_turf)
|
||||
if(parent.tracked_turf)
|
||||
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)
|
||||
RegisterSignal(new_turf, COMSIG_ATOM_ENTERED, PROC_REF(turflist_changed))
|
||||
RegisterSignal(new_turf, COMSIG_ATOM_EXITED, PROC_REF(turflist_changed))
|
||||
parent.stat_panel.send_message("create_listedturf", new_turf)
|
||||
parent.tracked_turf = new_turf
|
||||
|
||||
/datum/object_window_info/proc/stop_turf_tracking()
|
||||
if(GetComponent(/datum/component/connect_mob_behalf))
|
||||
qdel(GetComponent(/datum/component/connect_mob_behalf))
|
||||
if(parent.tracked_turf)
|
||||
UnregisterSignal(parent.tracked_turf, COMSIG_ATOM_ENTERED)
|
||||
UnregisterSignal(parent.tracked_turf, COMSIG_ATOM_EXITED)
|
||||
parent.stat_panel.send_message("remove_listedturf")
|
||||
parent.tracked_turf = null
|
||||
|
||||
/datum/object_window_info/proc/on_mob_move(mob/source)
|
||||
SIGNAL_HANDLER
|
||||
if(!parent.tracked_turf || !source.TurfAdjacent(parent.tracked_turf))
|
||||
source.set_listed_turf(null)
|
||||
|
||||
/datum/object_window_info/proc/on_mob_logout(mob/source)
|
||||
SIGNAL_HANDLER
|
||||
on_mob_move(parent.mob)
|
||||
|
||||
/datum/object_window_info/proc/turflist_changed(mob/source)
|
||||
SIGNAL_HANDLER
|
||||
if(!parent)//statbrowser hasnt fired yet and we still have a pending action
|
||||
return
|
||||
if(!(flags & TURFLIST_UPDATED)) //Limit updates to 1 per tick
|
||||
SSstatpanels.immediate_send_stat_data(parent)
|
||||
flags |= TURFLIST_UPDATED
|
||||
else if(!(flags & TURFLIST_UPDATE_QUEUED))
|
||||
flags |= TURFLIST_UPDATE_QUEUED
|
||||
|
||||
/// 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)
|
||||
if(!client)
|
||||
return
|
||||
if(!client.obj_window)
|
||||
client.obj_window = new(client)
|
||||
if(client.tracked_turf == new_turf)
|
||||
return
|
||||
if(!new_turf)
|
||||
client.obj_window.stop_turf_tracking() //Needs to go before listed_turf is set to null so signals can be removed
|
||||
return
|
||||
client.obj_window.start_turf_tracking(new_turf)
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
|
||||
if(new_character.client)
|
||||
new_character.client.init_verbs() // re-initialize character specific verbs
|
||||
new_character.set_listed_turf(null)
|
||||
|
||||
/datum/mind/proc/store_memory(new_text)
|
||||
memory += "[new_text]<BR>"
|
||||
|
||||
@@ -398,6 +398,7 @@
|
||||
|
||||
// called just as an item is picked up (loc is not yet changed)
|
||||
/obj/item/proc/pickup(mob/user)
|
||||
SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user)
|
||||
pixel_x = 0
|
||||
pixel_y = 0
|
||||
return
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
var/pass_color = FALSE // Will the item pass its own color var to the created item? Dyed cloth, wood, etc.
|
||||
var/strict_color_stacking = FALSE // Will the stack merge with other stacks that are different colors? (Dyed cloth, wood, etc)
|
||||
var/is_building = FALSE
|
||||
|
||||
/obj/item/stack/Initialize(mapload, var/starting_amount)
|
||||
. = ..()
|
||||
@@ -158,6 +159,9 @@
|
||||
var/required = quantity*recipe.req_amount
|
||||
var/produced = min(quantity*recipe.res_amount, recipe.max_res_amount)
|
||||
|
||||
if(is_building)
|
||||
return
|
||||
|
||||
if (!can_use(required))
|
||||
if (produced>1)
|
||||
to_chat(user, span_warning("You haven't got enough [src] to build \the [produced] [recipe.title]\s!"))
|
||||
@@ -175,9 +179,12 @@
|
||||
|
||||
if (recipe.time)
|
||||
to_chat(user, span_notice("Building [recipe.title] ..."))
|
||||
is_building = TRUE
|
||||
if (!do_after(user, recipe.time))
|
||||
is_building = FALSE
|
||||
return
|
||||
|
||||
is_building = FALSE
|
||||
if (use(required))
|
||||
var/atom/O
|
||||
if(recipe.use_material)
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
to_chat(user, span_notice("You can taste the sweet flavor of digital friendship. Or maybe it is something else."))
|
||||
|
||||
/obj/item/reagent_containers/food/after_trash_eaten(var/mob/living/user)
|
||||
if(!reagents.total_volume)
|
||||
if(!reagents?.total_volume)
|
||||
to_chat(user, span_notice("You can taste the flavor of garbage and leftovers. Delicious?"))
|
||||
else
|
||||
to_chat(user, span_notice("You can taste the flavor of gluttonous waste of food."))
|
||||
|
||||
@@ -184,3 +184,6 @@
|
||||
|
||||
/// The DPI scale of the client. 1 is equivalent to 100% window scaling, 2 will be 200% window scaling
|
||||
var/window_scaling
|
||||
|
||||
/// Loot panel for the client
|
||||
var/datum/lootpanel/loot_panel
|
||||
|
||||
@@ -305,6 +305,8 @@
|
||||
tgui_say.initialize()
|
||||
tgui_shocker.initialize()
|
||||
|
||||
loot_panel = new(src)
|
||||
|
||||
connection_time = world.time
|
||||
connection_realtime = world.realtime
|
||||
connection_timeofday = world.timeofday
|
||||
@@ -392,6 +394,7 @@
|
||||
GLOB.directory -= ckey
|
||||
GLOB.clients -= src
|
||||
|
||||
QDEL_NULL(loot_panel)
|
||||
..()
|
||||
return QDEL_HINT_HARDDEL_NOW
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
/datum/category_item/player_setup_item/general/nif/copy_to_mob(var/mob/living/carbon/human/character)
|
||||
//If you had a NIF...
|
||||
if(istype(character) && ispath(pref.nif_path) && pref.nif_durability)
|
||||
if(istype(character) && ispath(pref.nif_path) && pref.nif_durability && !ismannequin(character))
|
||||
new pref.nif_path(character, pref.nif_durability, pref.nif_savedata)
|
||||
|
||||
/datum/category_item/player_setup_item/general/nif/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
|
||||
80
code/modules/lootpanel/_lootpanel.dm
Normal file
80
code/modules/lootpanel/_lootpanel.dm
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* ## 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/tgui_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/tgui_close(mob/user)
|
||||
. = ..()
|
||||
|
||||
source_turf = null
|
||||
reset_contents()
|
||||
|
||||
|
||||
/datum/lootpanel/tgui_data(mob/user)
|
||||
var/list/data = list()
|
||||
|
||||
data["contents"] = get_contents()
|
||||
data["is_blind"] = !!user.is_blind()
|
||||
data["searching"] = length(to_image)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
/datum/lootpanel/tgui_status(mob/user, datum/tgui_state/state)
|
||||
// note: different from /tg/, we prohibit non-viewers from trying to update the window and close it automatically for them
|
||||
if(!(user in viewers(source_turf)))
|
||||
return STATUS_CLOSE
|
||||
|
||||
if(user.incapacitated())
|
||||
return STATUS_DISABLED
|
||||
|
||||
return STATUS_INTERACTIVE
|
||||
|
||||
|
||||
/datum/lootpanel/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("grab")
|
||||
return grab(usr, params)
|
||||
if("refresh")
|
||||
return populate_contents()
|
||||
|
||||
return FALSE
|
||||
54
code/modules/lootpanel/contents.dm
Normal file
54
code/modules/lootpanel/contents.dm
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Adds the item to contents and to_image (if needed)
|
||||
/datum/lootpanel/proc/add_to_index(datum/search_object/index)
|
||||
RegisterSignal(index, COMSIG_PARENT_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(!istype(thing))
|
||||
stack_trace("Non-atom in the contents of [source_turf]!")
|
||||
continue
|
||||
if(QDELETED(thing))
|
||||
continue
|
||||
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_PARENT_QDELETING)
|
||||
qdel(index)
|
||||
19
code/modules/lootpanel/handlers.dm
Normal file
19
code/modules/lootpanel/handlers.dm
Normal file
@@ -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()
|
||||
48
code/modules/lootpanel/misc.dm
Normal file
48
code/modules/lootpanel/misc.dm
Normal file
@@ -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, 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()
|
||||
tgui_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)
|
||||
89
code/modules/lootpanel/search_object.dm
Normal file
89
code/modules/lootpanel/search_object.dm
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* ## 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
|
||||
// Lest we find ourselves here again, this is intentionally stupid.
|
||||
// It tracks items going out and user actions, otherwise they can refresh the lootpanel.
|
||||
// If this is to be made to track everything, we'll need to make a new signal to specifically create/delete a search object
|
||||
RegisterSignals(item, list(
|
||||
COMSIG_ITEM_PICKUP,
|
||||
COMSIG_MOVABLE_MOVED,
|
||||
COMSIG_PARENT_QDELETING,
|
||||
), PROC_REF(on_item_moved))
|
||||
|
||||
// Icon generation conditions //////////////
|
||||
// Condition 1: Icon is complex
|
||||
if(ismob(item) || length(item.overlays) > 0)
|
||||
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))
|
||||
icon = "n/a"
|
||||
return
|
||||
|
||||
icon = "[item.icon]"
|
||||
icon_state = item.icon_state
|
||||
|
||||
|
||||
/datum/search_object/Destroy(force)
|
||||
item = null
|
||||
icon = null
|
||||
|
||||
return ..()
|
||||
|
||||
|
||||
/// Generates the icon for the search object. This is the expensive part.
|
||||
/datum/search_object/proc/generate_icon(client/owner)
|
||||
icon = costly_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
|
||||
|
||||
if(QDELETED(src))
|
||||
return
|
||||
|
||||
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(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src)
|
||||
40
code/modules/lootpanel/ss_looting.dm
Normal file
40
code/modules/lootpanel/ss_looting.dm
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
/// Queues image generation for search objects without icons
|
||||
SUBSYSTEM_DEF(looting)
|
||||
name = "Loot Icon Generation"
|
||||
flags = SS_NO_INIT
|
||||
priority = FIRE_PRIORITY_PROCESS
|
||||
runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT
|
||||
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--
|
||||
47
code/modules/lootpanel/ui.dm
Normal file
47
code/modules/lootpanel/ui.dm
Normal file
@@ -0,0 +1,47 @@
|
||||
/// 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
|
||||
|
||||
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;"
|
||||
if(params["alt"])
|
||||
modifiers += "alt=1;"
|
||||
if(params["right"])
|
||||
modifiers += "right=1;"
|
||||
|
||||
|
||||
user.ClickOn(thing, modifiers)
|
||||
|
||||
return TRUE
|
||||
@@ -744,11 +744,6 @@
|
||||
var/mob/M = locate(href_list["lookmob"])
|
||||
src.examinate(M)
|
||||
|
||||
if (href_list["clickitem"])
|
||||
var/obj/item/I = locate(href_list["clickitem"])
|
||||
if(src.client)
|
||||
src.ClickOn(I)
|
||||
|
||||
if (href_list["flavor_change"])
|
||||
switch(href_list["flavor_change"])
|
||||
if("done")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
269
tgui/packages/tgui/hotkeys.ts
Normal file
269
tgui/packages/tgui/hotkeys.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { globalEvents, type KeyEvent } from 'tgui-core/events';
|
||||
import * as keycodes from 'tgui-core/keycodes';
|
||||
|
||||
import { logger } from './logging';
|
||||
|
||||
// BYOND macros, in `key: command` format.
|
||||
const byondMacros: Record<string, string> = {};
|
||||
|
||||
// Default set of acquired keys, which will not be sent to BYOND.
|
||||
const hotKeysAcquired = [
|
||||
keycodes.KEY_ESCAPE,
|
||||
keycodes.KEY_ENTER,
|
||||
keycodes.KEY_SPACE,
|
||||
keycodes.KEY_TAB,
|
||||
keycodes.KEY_CTRL,
|
||||
keycodes.KEY_SHIFT,
|
||||
keycodes.KEY_UP,
|
||||
keycodes.KEY_DOWN,
|
||||
keycodes.KEY_LEFT,
|
||||
keycodes.KEY_RIGHT,
|
||||
keycodes.KEY_F5,
|
||||
];
|
||||
|
||||
// State of passed-through keys.
|
||||
const keyState: Record<string, boolean> = {};
|
||||
|
||||
// Custom listeners for key events
|
||||
const keyListeners: ((key: KeyEvent) => void)[] = [];
|
||||
|
||||
/**
|
||||
* Converts a browser keycode to BYOND keycode.
|
||||
*/
|
||||
function keyCodeToByond(keyCode: number) {
|
||||
if (keyCode === 16) return 'Shift';
|
||||
if (keyCode === 17) return 'Ctrl';
|
||||
if (keyCode === 18) return 'Alt';
|
||||
if (keyCode === 33) return 'Northeast';
|
||||
if (keyCode === 34) return 'Southeast';
|
||||
if (keyCode === 35) return 'Southwest';
|
||||
if (keyCode === 36) return 'Northwest';
|
||||
if (keyCode === 37) return 'West';
|
||||
if (keyCode === 38) return 'North';
|
||||
if (keyCode === 39) return 'East';
|
||||
if (keyCode === 40) return 'South';
|
||||
if (keyCode === 45) return 'Insert';
|
||||
if (keyCode === 46) return 'Delete';
|
||||
|
||||
if ((keyCode >= 48 && keyCode <= 57) || (keyCode >= 65 && keyCode <= 90)) {
|
||||
return String.fromCharCode(keyCode);
|
||||
}
|
||||
if (keyCode >= 96 && keyCode <= 105) {
|
||||
return `Numpad${keyCode - 96}`;
|
||||
}
|
||||
if (keyCode >= 112 && keyCode <= 123) {
|
||||
return `F${keyCode - 111}`;
|
||||
}
|
||||
if (keyCode === 188) return ',';
|
||||
if (keyCode === 189) return '-';
|
||||
if (keyCode === 190) return '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard passthrough logic. This allows you to keep doing things
|
||||
* in game while the browser window is focused.
|
||||
*/
|
||||
function handlePassthrough(key: KeyEvent) {
|
||||
const keyString = String(key);
|
||||
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
|
||||
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
// Prevent passthrough on Ctrl+F
|
||||
if (keyString === 'Ctrl+F') {
|
||||
return;
|
||||
}
|
||||
// NOTE: Alt modifier is pretty bad and sticky in IE11.
|
||||
|
||||
if (
|
||||
key.event.defaultPrevented ||
|
||||
key.isModifierKey() ||
|
||||
hotKeysAcquired.includes(key.code) ||
|
||||
key.repeat // no repeating
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const byondKeyCode = keyCodeToByond(key.code);
|
||||
if (!byondKeyCode) {
|
||||
return;
|
||||
}
|
||||
let byondKeyCodeIdent = byondKeyCode;
|
||||
if (key.isUp()) {
|
||||
byondKeyCodeIdent += '+UP';
|
||||
}
|
||||
// Macro
|
||||
const macro = byondMacros[byondKeyCodeIdent];
|
||||
if (macro) {
|
||||
return Byond.command(macro);
|
||||
}
|
||||
// KeyDown
|
||||
if (key.isDown() && !keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = true;
|
||||
const command = keyPassthroughConfig.verbParamsFn(
|
||||
keyPassthroughConfig.keyDownVerb,
|
||||
byondKeyCode,
|
||||
);
|
||||
return Byond.command(command);
|
||||
}
|
||||
// KeyUp
|
||||
if (key.isUp() && keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
const command = keyPassthroughConfig.verbParamsFn(
|
||||
keyPassthroughConfig.keyUpVerb,
|
||||
byondKeyCode,
|
||||
);
|
||||
return Byond.command(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a lock on the hotkey, which prevents it from being
|
||||
* passed through to BYOND.
|
||||
*/
|
||||
export function acquireHotKey(keyCode: number) {
|
||||
hotKeysAcquired.push(keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the hotkey available to BYOND again.
|
||||
*/
|
||||
export function releaseHotKey(keyCode: number) {
|
||||
const index = hotKeysAcquired.indexOf(keyCode);
|
||||
if (index >= 0) {
|
||||
hotKeysAcquired.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseHeldKeys() {
|
||||
for (const byondKeyCode in keyState) {
|
||||
if (keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
Byond.command(
|
||||
keyPassthroughConfig.verbParamsFn(
|
||||
keyPassthroughConfig.keyUpVerb,
|
||||
byondKeyCode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ByondSkinMacro = {
|
||||
command: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
let keyPassthroughConfig: KeyPassthroughConfig = {
|
||||
keyDownVerb: 'KeyDown',
|
||||
keyUpVerb: 'KeyUp',
|
||||
verbParamsFn: (verb, keyCode) => `${verb} "${keyCode}"`,
|
||||
};
|
||||
|
||||
export type KeyPassthroughConfig = {
|
||||
keyUpVerb: string;
|
||||
keyDownVerb: string;
|
||||
verbParamsFn: (verb: string, keyCode: string) => string;
|
||||
};
|
||||
|
||||
export function setupHotKeys(config?: KeyPassthroughConfig) {
|
||||
if (config) {
|
||||
keyPassthroughConfig = config;
|
||||
}
|
||||
// Read macros
|
||||
Byond.winget(null, 'macros').then((data: string) => {
|
||||
const separated = data.split(';');
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
for (const set of separated) {
|
||||
promises.push(Byond.winget(set + '.*'));
|
||||
}
|
||||
|
||||
Promise.all(promises).then((sets: Record<string, string>[]) => {
|
||||
// Group each macro by ref
|
||||
const groupedByRef: Record<string, ByondSkinMacro> = {};
|
||||
for (const set of sets) {
|
||||
for (const key of Object.keys(set)) {
|
||||
const keyPath = key.split('.');
|
||||
const ref = keyPath[1];
|
||||
const prop = keyPath[2];
|
||||
|
||||
if (ref && prop) {
|
||||
// This piece of code imperatively adds each property to a
|
||||
// ByondSkinMacro object in the order we meet it, which is hard
|
||||
// to express safely in typescript.
|
||||
if (!groupedByRef[ref]) {
|
||||
groupedByRef[ref] = {} as any;
|
||||
}
|
||||
groupedByRef[ref][prop] = set[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert macros
|
||||
const escapedQuotRegex = /\\"/g;
|
||||
|
||||
function unEscape(str: string) {
|
||||
return str.substring(1, str.length - 1).replace(escapedQuotRegex, '"');
|
||||
}
|
||||
|
||||
for (const ref of Object.keys(groupedByRef)) {
|
||||
const macro = groupedByRef[ref];
|
||||
const byondKeyName = unEscape(macro.name);
|
||||
byondMacros[byondKeyName] = unEscape(macro.command);
|
||||
}
|
||||
|
||||
logger.log(byondMacros);
|
||||
});
|
||||
});
|
||||
|
||||
// Setup event handlers
|
||||
globalEvents.on('window-blur', () => {
|
||||
releaseHeldKeys();
|
||||
});
|
||||
globalEvents.on('input-focus', () => {
|
||||
releaseHeldKeys();
|
||||
});
|
||||
startKeyPassthrough();
|
||||
}
|
||||
|
||||
export function startKeyPassthrough() {
|
||||
globalEvents.on('key', keyEvent);
|
||||
}
|
||||
|
||||
export function stopKeyPassthrough() {
|
||||
globalEvents.off('key', keyEvent);
|
||||
}
|
||||
|
||||
function keyEvent(key: KeyEvent) {
|
||||
for (const keyListener of keyListeners) {
|
||||
keyListener(key);
|
||||
}
|
||||
handlePassthrough(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers for any key events, such as key down or key up.
|
||||
* This should be preferred over directly connecting to keydown/keyup
|
||||
* as it lets tgui prevent the key from reaching BYOND.
|
||||
*
|
||||
* If using in a component, prefer KeyListener, which automatically handles
|
||||
* stopping listening when unmounting.
|
||||
*
|
||||
* @param callback The function to call whenever a key event occurs
|
||||
* @returns A callback to stop listening
|
||||
*/
|
||||
export function listenForKeyEvents(callback: (key: KeyEvent) => void) {
|
||||
keyListeners.push(callback);
|
||||
|
||||
let removed = false;
|
||||
|
||||
return () => {
|
||||
if (removed) {
|
||||
return;
|
||||
}
|
||||
|
||||
removed = true;
|
||||
keyListeners.splice(keyListeners.indexOf(callback), 1);
|
||||
};
|
||||
}
|
||||
@@ -33,11 +33,11 @@ import './styles/themes/algae.scss';
|
||||
|
||||
import { perf } from 'common/perf';
|
||||
import { setupGlobalEvents } from 'tgui-core/events';
|
||||
import { setupHotKeys } from 'tgui-core/hotkeys';
|
||||
import { setupHotReloading } from 'tgui-dev-server/link/client';
|
||||
|
||||
import { App } from './App';
|
||||
import { setGlobalStore } from './backend';
|
||||
import { setupHotKeys } from './hotkeys';
|
||||
import { captureExternalLinks } from './links';
|
||||
import { render } from './renderer';
|
||||
import { configureStore } from './store';
|
||||
|
||||
26
tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx
Normal file
26
tgui/packages/tgui/interfaces/LootPanel/GroupedContents.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Box } from 'tgui-core/components';
|
||||
import { createSearch } from 'tgui-core/string';
|
||||
|
||||
import { LootBox } from './LootBox';
|
||||
import type { SearchGroup, SearchItem } from './types';
|
||||
|
||||
type Props = {
|
||||
contents: Record<string, SearchItem[]>;
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
export function GroupedContents(props: Props) {
|
||||
const { contents, searchText } = props;
|
||||
|
||||
const filteredContents: SearchGroup[] = Object.entries(contents)
|
||||
.filter(createSearch(searchText, ([_, items]) => items[0].name))
|
||||
.map(([_, items]) => ({ amount: items.length, item: items[0] }));
|
||||
|
||||
return (
|
||||
<Box m={-0.5}>
|
||||
{filteredContents.map((group) => (
|
||||
<LootBox key={group.item.name} group={group} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
44
tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx
Normal file
44
tgui/packages/tgui/interfaces/LootPanel/IconDisplay.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { DmIcon, Icon, Image } from 'tgui-core/components';
|
||||
|
||||
import type { SearchItem } from './types';
|
||||
|
||||
type Props = {
|
||||
item: SearchItem;
|
||||
size: Size;
|
||||
};
|
||||
|
||||
type Size = {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export function IconDisplay(props: Props) {
|
||||
const {
|
||||
item: { icon, icon_state },
|
||||
size: { height, width },
|
||||
} = props;
|
||||
|
||||
const fallback = <Icon name="spinner" size={1.5} spin color="gray" />;
|
||||
|
||||
if (!icon) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (icon === 'n/a') {
|
||||
return <Icon name="dumpster-fire" size={1.5} color="gray" />;
|
||||
}
|
||||
|
||||
if (icon_state) {
|
||||
return (
|
||||
<DmIcon
|
||||
fallback={fallback}
|
||||
icon={icon}
|
||||
icon_state={icon_state}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Image fixErrors src={icon} height={3} width={3} />;
|
||||
}
|
||||
78
tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx
Normal file
78
tgui/packages/tgui/interfaces/LootPanel/LootBox.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, Stack } from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
import { capitalizeFirst } from 'tgui-core/string';
|
||||
|
||||
import { IconDisplay } from './IconDisplay';
|
||||
import type { SearchGroup, SearchItem } from './types';
|
||||
|
||||
type Data = {
|
||||
is_blind: BooleanLike;
|
||||
};
|
||||
|
||||
type Props =
|
||||
| {
|
||||
item: SearchItem;
|
||||
}
|
||||
| {
|
||||
group: SearchGroup;
|
||||
};
|
||||
|
||||
export function LootBox(props: Props) {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { is_blind } = data;
|
||||
|
||||
let amount = 0;
|
||||
let item: SearchItem;
|
||||
if ('group' in props) {
|
||||
amount = props.group.amount;
|
||||
item = props.group.item;
|
||||
} else {
|
||||
item = props.item;
|
||||
}
|
||||
|
||||
const name = !item.name ? '???' : capitalizeFirst(item.name);
|
||||
|
||||
const content = (
|
||||
<Button
|
||||
p={0}
|
||||
fluid
|
||||
color="transparent"
|
||||
onClick={(event) =>
|
||||
act('grab', {
|
||||
alt: event.altKey,
|
||||
ctrl: event.ctrlKey,
|
||||
ref: item.ref,
|
||||
shift: event.shiftKey,
|
||||
})
|
||||
}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
act('grab', {
|
||||
right: true,
|
||||
ref: item.ref,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Stack.Item mb={-1} minWidth={'36px'} minHeight={'42px'}>
|
||||
<IconDisplay item={item} size={{ height: 3, width: 3 }} />
|
||||
</Stack.Item>
|
||||
<Stack.Item
|
||||
lineHeight="34px"
|
||||
overflow="hidden"
|
||||
style={{ textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{!is_blind && name}
|
||||
</Stack.Item>
|
||||
<Stack.Item lineHeight="34px" pr={1}>
|
||||
{amount > 1 && 'x' + amount}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (is_blind) return content;
|
||||
|
||||
return content;
|
||||
}
|
||||
26
tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx
Normal file
26
tgui/packages/tgui/interfaces/LootPanel/RawContents.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Box } from 'tgui-core/components';
|
||||
import { createSearch } from 'tgui-core/string';
|
||||
|
||||
import { LootBox } from './LootBox';
|
||||
import type { 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 (
|
||||
<Box m={-0.5}>
|
||||
{filteredContents.map((item) => (
|
||||
<LootBox key={item.ref} item={item} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
108
tgui/packages/tgui/interfaces/LootPanel/index.tsx
Normal file
108
tgui/packages/tgui/interfaces/LootPanel/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Button, Input, Section, Stack } from 'tgui-core/components';
|
||||
import { isEscape } from 'tgui-core/keys';
|
||||
import { clamp } from 'tgui-core/math';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
import { GroupedContents } from './GroupedContents';
|
||||
import { RawContents } from './RawContents';
|
||||
import type { SearchItem } from './types';
|
||||
|
||||
type Data = {
|
||||
contents: SearchItem[];
|
||||
searching: BooleanLike;
|
||||
};
|
||||
|
||||
export function LootPanel(props) {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { contents = [], searching } = data;
|
||||
|
||||
// limitations: items with different stack counts, charges etc.
|
||||
const contentsByPathName = 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 + item.name]) {
|
||||
acc[item.path + item.name] = [];
|
||||
}
|
||||
acc[item.path + item.name].push(item);
|
||||
} else {
|
||||
acc[item.ref] = [item];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [contents]);
|
||||
|
||||
const [grouping, setGrouping] = useState(true);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const headerHeight = 38;
|
||||
const itemHeight = 38;
|
||||
const minHeight = headerHeight + itemHeight;
|
||||
const maxHeight = headerHeight + itemHeight * 10;
|
||||
const height: number = clamp(
|
||||
headerHeight +
|
||||
(!grouping ? contents.length : Object.keys(contentsByPathName).length) *
|
||||
itemHeight,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
);
|
||||
|
||||
return (
|
||||
<Window
|
||||
width={350}
|
||||
height={height}
|
||||
buttons={
|
||||
<Stack align="center">
|
||||
<Input
|
||||
onChange={setSearchText}
|
||||
placeholder="Search items..."
|
||||
value={searchText}
|
||||
/>
|
||||
<Button
|
||||
m={0}
|
||||
icon={grouping ? 'layer-group' : 'object-ungroup'}
|
||||
selected={grouping}
|
||||
onClick={() => setGrouping(!grouping)}
|
||||
tooltip="Toggle Grouping"
|
||||
/>
|
||||
<Button
|
||||
icon="sync"
|
||||
onClick={() => act('refresh')}
|
||||
tooltip="Refresh"
|
||||
/>
|
||||
<Button
|
||||
icon="circle-question"
|
||||
tooltip="If you need keybinds to work while this menu is focused, hit Ctrl+R."
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Window.Content
|
||||
fitted
|
||||
scrollable={height === maxHeight}
|
||||
onKeyDown={(event) => {
|
||||
if (isEscape(event.key)) {
|
||||
Byond.sendMessage('close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
{grouping ? (
|
||||
<GroupedContents
|
||||
contents={contentsByPathName}
|
||||
searchText={searchText}
|
||||
/>
|
||||
) : (
|
||||
<RawContents contents={contents} searchText={searchText} />
|
||||
)}
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
13
tgui/packages/tgui/interfaces/LootPanel/types.ts
Normal file
13
tgui/packages/tgui/interfaces/LootPanel/types.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -3032,6 +3032,13 @@
|
||||
#include "code\modules\looking_glass\lg_console.dm"
|
||||
#include "code\modules\looking_glass\lg_imageholder.dm"
|
||||
#include "code\modules\looking_glass\lg_turfs.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\lore_codex\codex.dm"
|
||||
#include "code\modules\lore_codex\pages.dm"
|
||||
#include "code\modules\lore_codex\lore_data_yw\important_locations.dm"
|
||||
|
||||
Reference in New Issue
Block a user