TGUI Select Equipment menu (#57673)

Co-authored-by: Triggeredboi <lucas.moreno2124@gmail.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
This commit is contained in:
Trigg
2021-03-15 19:39:02 -07:00
committed by GitHub
co-authored by Triggeredboi Aleksej Komarov
parent 3bbad6e878
commit 8a5c7c12e3
10 changed files with 483 additions and 42 deletions
+2 -3
View File
@@ -1061,10 +1061,9 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
COMPILE_OVERLAYS(body)
for(var/D in showDirs)
body.setDir(D)
COMPILE_OVERLAYS(body)
var/icon/partial = getFlatIcon(body)
var/icon/partial = getFlatIcon(body, defdir=D)
out_icon.Insert(partial,dir=D)
humanoid_icon_cache[icon_id] = out_icon
+2 -2
View File
@@ -84,7 +84,7 @@ GLOBAL_PROTECT(admin_verbs_ban)
GLOBAL_LIST_INIT(admin_verbs_sounds, list(/client/proc/play_local_sound, /client/proc/play_direct_mob_sound, /client/proc/play_sound, /client/proc/set_round_end_sound))
GLOBAL_PROTECT(admin_verbs_sounds)
GLOBAL_LIST_INIT(admin_verbs_fun, list(
/client/proc/cmd_admin_dress,
/client/proc/cmd_select_equipment,
/client/proc/cmd_admin_gib_self,
/client/proc/drop_bomb,
/client/proc/set_dynex_scale,
@@ -217,7 +217,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
/client/proc/play_local_sound,
/client/proc/play_sound,
/client/proc/set_round_end_sound,
/client/proc/cmd_admin_dress,
/client/proc/cmd_select_equipment,
/client/proc/cmd_admin_gib_self,
/client/proc/drop_bomb,
/client/proc/drop_dynex_bomb,
-33
View File
@@ -495,39 +495,6 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
set name = "Test Areas (ALL)"
cmd_admin_areatest(FALSE)
/client/proc/cmd_admin_dress(mob/M in GLOB.mob_list)
set category = "Admin.Events"
set name = "Select equipment"
if(!(ishuman(M) || isobserver(M)))
alert("Invalid mob")
return
var/dresscode = robust_dress_shop()
if(!dresscode)
return
var/delete_pocket
var/mob/living/carbon/human/H
if(isobserver(M))
H = M.change_mob_type(/mob/living/carbon/human, null, null, TRUE)
else
H = M
if(H.l_store || H.r_store || H.s_store) //saves a lot of time for admins and coders alike
if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No")
delete_pocket = TRUE
SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
for (var/obj/item/I in H.get_equipped_items(delete_pocket))
qdel(I)
if(dresscode != "Naked")
H.equipOutfit(dresscode)
H.regenerate_icons()
log_admin("[key_name(usr)] changed the equipment of [key_name(H)] to [dresscode].")
message_admins("<span class='adminnotice'>[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(H)] to [dresscode].</span>")
/client/proc/robust_dress_shop()
var/list/baseoutfits = list("Naked","Custom","As Job...", "As Plasmaman...")
+230
View File
@@ -0,0 +1,230 @@
/client/proc/cmd_select_equipment(mob/target in GLOB.mob_list)
set category = "Admin.Events"
set name = "Select equipment"
var/datum/select_equipment/ui = new(usr, target)
ui.ui_interact(usr)
/*
* This is the datum housing the select equipment UI.
*
* You may notice some oddities about the way outfits are passed to the UI and vice versa here.
* That's because it handles both outfit typepaths (for normal outfits) *and* outfit objects (for custom outfits).
*
* Custom outfits need to be objects as they're created in runtime.
* "Then just handle the normal outfits as objects too and simplify the handling" - you may say.
* There are about 300 outfit types at the time of writing this. Initializing all of these to objects would be a huge waste.
*
*/
/datum/select_equipment
var/client/user
var/mob/target_mob
var/dummy_key
var/mob/living/carbon/human/dummy/dummy
//static list to share all the outfit typepaths between all instances of this datum.
var/static/list/cached_outfits
//a typepath if the selected outfit is a normal outfit;
//an object if the selected outfit is a custom outfit
var/datum/outfit/selected_outfit = /datum/outfit
//serializable string for the UI to keep track of which outfit is selected
var/selected_identifier = "/datum/outfit"
/datum/select_equipment/New(_user, mob/target)
user = CLIENT_FROM_VAR(_user)
if(!ishuman(target) && !isobserver(target))
alert("Invalid mob")
return
target_mob = target
/datum/select_equipment/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "SelectEquipment", "Select Equipment")
ui.open()
ui.set_autoupdate(FALSE)
/datum/select_equipment/ui_state(mob/user)
return GLOB.admin_state
/datum/select_equipment/ui_status(mob/user, datum/ui_state/state)
if(QDELETED(target_mob))
return UI_CLOSE
return ..()
/datum/select_equipment/ui_close(mob/user)
clear_human_dummy(dummy_key)
qdel(src)
/datum/select_equipment/proc/init_dummy()
dummy_key = "selectequipmentUI_[target_mob]"
dummy = generate_or_wait_for_human_dummy(dummy_key)
var/mob/living/carbon/carbon_target = target_mob
if(istype(carbon_target))
carbon_target.dna.transfer_identity(dummy)
dummy.updateappearance()
unset_busy_human_dummy(dummy_key)
return
/**
* Packs up data about an outfit as an assoc list to send to the UI as an outfit entry.
*
* Args:
* * category (string) - The tab it will be under
*
* * identifier (typepath or ref) - This will sent this back to ui_act to preview or spawn in an outfit.
* * Must be unique between all entries.
*
* * name (string) - Will be the text on the button
*
* * priority (bool)(optional) - If True, the UI will sort the entry to the top, right below favorites.
*
* * custom_entry (bool)(optional) - Send the identifier with a "ref" keyword instead of "path",
* * for the UI to tell apart custom outfits from normal ones.
*
* Returns (list) An outfit entry
*/
/datum/select_equipment/proc/outfit_entry(category, identifier, name, priority=FALSE, custom_entry=FALSE)
if(custom_entry)
return list("category" = category, "ref" = identifier, "name" = name, "priority" = priority)
return list("category" = category, "path" = identifier, "name" = name, "priority" = priority)
/datum/select_equipment/proc/make_outfit_entries(category="General", list/outfit_list)
var/list/entries = list()
for(var/path as anything in outfit_list)
var/datum/outfit/outfit = path
entries += list(outfit_entry(category, path, initial(outfit.name)))
return entries
//GLOB.custom_outfits lists outfit *objects* so we'll need to do some custom handling for it
/datum/select_equipment/proc/make_custom_outfit_entries(list/outfit_list)
var/list/entries = list()
for(var/datum/outfit/outfit as anything in outfit_list)
entries += list(outfit_entry("Custom", REF(outfit), outfit.name, custom_entry=TRUE)) //it's either this or special handling on the UI side
return entries
/datum/select_equipment/ui_data(mob/user)
var/list/data = list()
if(!dummy)
init_dummy()
var/datum/preferences/prefs = target_mob?.client?.prefs
var/icon/dummysprite = get_flat_human_icon(null, prefs=prefs, dummy_key = dummy_key, outfit_override = selected_outfit)
data["icon64"] = icon2base64(dummysprite)
data["name"] = target_mob
data["favorites"] = list()
if(prefs)
data["favorites"] = prefs.favorite_outfits
var/list/custom
custom += make_custom_outfit_entries(GLOB.custom_outfits)
data["custom_outfits"] = custom
data["current_outfit"] = selected_identifier
return data
/datum/select_equipment/ui_static_data(mob/user)
var/list/data = list()
if(!cached_outfits)
cached_outfits = list()
cached_outfits += list(outfit_entry("General", /datum/outfit, "Naked", priority=TRUE))
cached_outfits += make_outfit_entries("General", subtypesof(/datum/outfit) - typesof(/datum/outfit/job) - typesof(/datum/outfit/plasmaman))
cached_outfits += make_outfit_entries("Jobs", typesof(/datum/outfit/job))
cached_outfits += make_outfit_entries("Plasmamen Outfits", typesof(/datum/outfit/plasmaman))
data["outfits"] = cached_outfits
return data
/datum/select_equipment/proc/resolve_outfit(text)
var/path = text2path(text)
if(ispath(path, /datum/outfit))
return path
else //don't bail yet - could be a custom outfit
var/datum/outfit/custom_outfit = locate(text)
if(istype(custom_outfit))
return custom_outfit
/datum/select_equipment/ui_act(action, params)
if(..())
return
. = TRUE
switch(action)
if("preview")
var/datum/outfit/new_outfit = resolve_outfit(params["path"])
if(ispath(new_outfit)) //got a typepath - that means we're dealing with a normal outfit
selected_identifier = new_outfit //these are keyed by type
//by the way, no, they can't be keyed by name because many of them have duplicate names
else if(istype(new_outfit)) //got an initialized object - means it's a custom outfit
selected_identifier = REF(new_outfit) //and the outfit will be keyed by its ref (cause its type will always be /datum/outfit)
else //we got nothing and should bail
return
selected_outfit = new_outfit
if("applyoutfit")
var/datum/outfit/new_outfit = resolve_outfit(params["path"])
if(new_outfit && ispath(new_outfit)) //initialize it
new_outfit = new new_outfit
if(!istype(new_outfit))
return
user.admin_apply_outfit(target_mob, new_outfit)
if("customoutfit")
user.outfit_manager()
if("togglefavorite")
var/datum/outfit/outfit_path = resolve_outfit(params["path"])
if(!ispath(outfit_path)) //we do *not* want custom outfits (i.e objects) here, they're not even persistent
return
if(user.prefs.favorite_outfits.Find(outfit_path)) //already there, remove it
user.prefs.favorite_outfits -= outfit_path
else //not there, add it
user.prefs.favorite_outfits += outfit_path
user.prefs.save_preferences()
/client/proc/admin_apply_outfit(mob/target, dresscode)
if(!ishuman(target) && !isobserver(target))
alert("Invalid mob")
return
if(!dresscode)
return
var/delete_pocket
var/mob/living/carbon/human/human_target
if(isobserver(target))
human_target = target.change_mob_type(/mob/living/carbon/human, delete_old_mob = TRUE)
else
human_target = target
if(human_target.l_store || human_target.r_store || human_target.s_store) //saves a lot of time for admins and coders alike
if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No")
delete_pocket = TRUE
SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
for(var/obj/item/item in human_target.get_equipped_items(delete_pocket))
qdel(item)
if(dresscode != "Naked")
human_target.equipOutfit(dresscode)
human_target.regenerate_icons()
log_admin("[key_name(usr)] changed the equipment of [key_name(human_target)] to [dresscode].")
message_admins("<span class='adminnotice'>[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(human_target)] to [dresscode].</span>")
return dresscode
+3 -1
View File
@@ -146,6 +146,8 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/persistent_scars = TRUE
///If we want to broadcast deadchat connect/disconnect messages
var/broadcast_login_logout = TRUE
///What outfit typepaths we've favorited in the SelectEquipment menu
var/list/favorite_outfits = list()
/datum/preferences/New(client/C)
parent = C
@@ -645,7 +647,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
dat += "<b>Set screentip mode:</b> <a href='?_src_=prefs;preference=screentipmode'>[screentip_pref ? "Enabled" : "Disabled"]</a><br>"
dat += "<b>Screentip color:</b><span style='border: 1px solid #161616; background-color: [screentip_color];'>&nbsp;&nbsp;&nbsp;</span> <a href='?_src_=prefs;preference=screentipcolor'>Change</a><BR>"
dat += "<b>Item Hover Outlines:</b> <a href='?_src_=prefs;preference=itemoutline_pref'>[itemoutline_pref ? "Enabled" : "Disabled"]</a><br>"
dat += "<b>Ambient Occlusion:</b> <a href='?_src_=prefs;preference=ambientocclusion'>[ambientocclusion ? "Enabled" : "Disabled"]</a><br>"
dat += "<b>Fit Viewport:</b> <a href='?_src_=prefs;preference=auto_fit_viewport'>[auto_fit_viewport ? "Auto" : "Manual"]</a><br>"
@@ -214,6 +214,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
READ_FILE(S["hearted_until"], hearted_until)
if(hearted_until > world.realtime)
hearted = TRUE
//favorite outfits
READ_FILE(S["favorite_outfits"], favorite_outfits)
var/list/parsed_favs = list()
for(var/typetext in favorite_outfits)
var/datum/outfit/path = text2path(typetext)
if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore
parsed_favs += path
favorite_outfits = uniqueList(parsed_favs)
//try to fix any outdated data if necessary
if(needs_update >= 0)
@@ -262,6 +271,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
pda_style = sanitize_inlist(pda_style, GLOB.pda_styles, initial(pda_style))
pda_color = sanitize_hexcolor(pda_color, 6, 1, initial(pda_color))
key_bindings = sanitize_keybindings(key_bindings)
favorite_outfits = SANITIZE_LIST(favorite_outfits)
if(needs_update >= 0) //save the updated version
var/old_default_slot = default_slot
@@ -339,6 +349,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
WRITE_FILE(S["pda_color"], pda_color)
WRITE_FILE(S["key_bindings"], key_bindings)
WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null))
WRITE_FILE(S["favorite_outfits"], favorite_outfits)
return TRUE
/datum/preferences/proc/load_character(slot)
+16 -3
View File
@@ -40,13 +40,26 @@ GLOBAL_LIST_EMPTY(dummy_mob_list)
D = new
GLOB.human_dummy_list[slotkey] = D
GLOB.dummy_mob_list += D
else
D.regenerate_icons() //they were cut in wipe_state()
D.in_use = TRUE
return D
/proc/unset_busy_human_dummy(slotnumber)
if(!slotnumber)
/proc/unset_busy_human_dummy(slotkey)
if(!slotkey)
return
var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotnumber]
var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotkey]
if(istype(D))
D.wipe_state()
D.in_use = FALSE
/proc/clear_human_dummy(slotkey)
if(!slotkey)
return
var/mob/living/carbon/human/dummy/dummy = GLOB.human_dummy_list[slotkey]
GLOB.human_dummy_list -= slotkey
if(istype(dummy))
GLOB.dummy_mob_list -= dummy
qdel(dummy)
+1
View File
@@ -1541,6 +1541,7 @@
#include "code\modules\admin\verbs\randomverbs.dm"
#include "code\modules\admin\verbs\reestablish_db_connection.dm"
#include "code\modules\admin\verbs\secrets.dm"
#include "code\modules\admin\verbs\selectequipment.dm"
#include "code\modules\admin\verbs\shuttlepanel.dm"
#include "code\modules\admin\verbs\spawnobjasmob.dm"
#include "code\modules\admin\verbs\tripAI.dm"
+4
View File
@@ -171,6 +171,8 @@ export const sortBy = (...iterateeFns) => array => {
return mappedArray;
};
export const sort = sortBy();
/**
* A fast implementation of reduce.
*/
@@ -235,6 +237,8 @@ export const uniqBy = iterateeFn => array => {
return result;
};
export const uniq = uniqBy();
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
@@ -0,0 +1,214 @@
import { filter, map, sortBy, uniq } from 'common/collections';
import { flow } from 'common/fp';
import { createSearch } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Icon, Input, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
// here's an important mental define:
// custom outfits give a ref keyword instead of path
const getOutfitKey = outfit => outfit.path || outfit.ref;
const useOutfitTabs = (context, categories) => {
return useLocalState(context, 'selected-tab', categories[0]);
};
export const SelectEquipment = (props, context) => {
const { act, data } = useBackend(context);
const {
name,
icon64,
current_outfit,
favorites,
} = data;
const isFavorited = entry => favorites?.includes(entry.path);
const outfits = map(entry => ({
...entry,
favorite: isFavorited(entry),
}))([
...data.outfits,
...data.custom_outfits,
]);
// even if no custom outfits were sent, we still want to make sure there's
// at least a 'Custom' tab so the button to create a new one pops up
const categories = uniq([
...outfits.map(entry => entry.category),
'Custom',
]);
const [tab] = useOutfitTabs(context, categories);
const [searchText, setSearchText] = useLocalState(
context, 'searchText', '');
const searchFilter = createSearch(searchText, entry => (
entry.name + entry.path
));
const visibleOutfits = flow([
filter(entry => entry.category === tab),
filter(searchFilter),
sortBy(
entry => !entry.favorite,
entry => !entry.priority,
entry => entry.name
),
])(outfits);
const getOutfitEntry = current_outfit => outfits.find(outfit => (
getOutfitKey(outfit) === current_outfit
));
const currentOutfitEntry = getOutfitEntry(current_outfit);
return (
<Window
width={650}
height={415}>
<Window.Content>
<Stack fill>
<Stack.Item>
<Stack fill vertical>
<Stack.Item>
<Input
fluid
autoFocus
placeholder="Search"
value={searchText}
onInput={(e, value) => setSearchText(value)} />
</Stack.Item>
<Stack.Item>
<DisplayTabs categories={categories} />
</Stack.Item>
<Stack.Item mt={0} grow={1} basis={0}>
<OutfitDisplay
entries={visibleOutfits}
currentTab={tab} />
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
<Stack fill vertical>
<Stack.Item>
<Section>
<CurrentlySelectedDisplay entry={currentOutfitEntry} />
</Section>
</Stack.Item>
<Stack.Item grow={1}>
<Section
fill
title={name}
textAlign="center">
<Box
as="img"
m={0}
src={`data:image/jpeg;base64,${icon64}`}
height="100%"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const DisplayTabs = (props, context) => {
const { categories } = props;
const [tab, setTab] = useOutfitTabs(context, categories);
return (
<Tabs textAlign="center">
{categories.map(category => (
<Tabs.Tab
key={category}
selected={tab === category}
onClick={() => setTab(category)}>
{category}
</Tabs.Tab>
))}
</Tabs>
);
};
const OutfitDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entries, currentTab } = props;
return (
<Section fill scrollable>
{entries.map(entry => (
<Button
key={getOutfitKey(entry)}
fluid
ellipsis
icon={entry.favorite && 'star'}
iconColor="gold"
content={entry.name}
title={entry.path || entry.name}
selected={getOutfitKey(entry) === current_outfit}
onClick={() => act('preview', { path: getOutfitKey(entry) })} />
))}
{currentTab === "Custom" && (
<Button
color="transparent"
icon="plus"
fluid
onClick={() => act('customoutfit')}>
Create a custom outfit...
</Button>
)}
</Section>
);
};
const CurrentlySelectedDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entry } = props;
return (
<Stack align="center">
{entry?.path && (
<Stack.Item>
<Icon
size={1.6}
name={entry.favorite ? 'star' : 'star-o'}
color="gold"
style={{ cursor: 'pointer' }}
onClick={() => act('togglefavorite', {
path: entry.path,
})} />
</Stack.Item>
)}
<Stack.Item grow={1} basis={0}>
<Box color="label">
Currently selected:
</Box>
<Box
title={entry?.path}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
{entry?.name}
</Box>
</Stack.Item>
<Stack.Item>
<Button
mr={0.8}
lineHeight={2}
color="green"
onClick={() => act('applyoutfit', {
path: current_outfit,
})}>
Confirm
</Button>
</Stack.Item>
</Stack>
);
};