[MDB Ignore] Updates visuals for the loadout menu (#90399)

## About The Pull Request

Improves how the loadout menu looks by switching it to the new
ImageButton component, converting recolorable/reskinnable to use icons
with tooltips, and allowing items within a category to be grouped up
together (only used by pocket items as of now)

<details>

<summary>How it looks:</summary>


![image](https://github.com/user-attachments/assets/3ed434cc-5caf-4cd5-b289-76f1946282cd)


![image](https://github.com/user-attachments/assets/bb33e6a4-75c4-4eb5-8e83-2ed9f6853e64)

</details>

Monocles have been made prescription, and prescription, contains pens,
hair color, and other misc tags have been removed.



<details>

<summary>Additionally, plushies now have better baked-in sprites for
loadouts, with randomized lizard plushie receiving a coat of rainbow
paint in place of the removed tooltip.</summary>


![image](https://github.com/user-attachments/assets/ea3124ae-53aa-4eb5-bb27-bde6140b4222)

</details>

## Why It's Good For The Game

Blue button make it hard to figure out details on certain items, and
tiny grey text is borderline unreadable. Also our pocket items category
is horribly bloated.

## Changelog
🆑
qol: Redesigned the loadout UI
balance: Monocles now work as prescription glasses
image: Loadout plushies no longer use mapper previews for their images
/🆑
This commit is contained in:
SmArtKar
2025-04-17 18:25:06 +02:00
committed by GitHub
parent c34057b49b
commit 71a4f3a83f
11 changed files with 263 additions and 196 deletions

View File

@@ -24,8 +24,6 @@ SUBSYSTEM_DEF(assets)
transport = newtransport
transport.Load()
/datum/controller/subsystem/assets/Initialize()
for(var/type in typesof(/datum/asset))
var/datum/asset/A = type

View File

@@ -255,6 +255,7 @@
inhand_icon_state = "headset" // lol
lefthand_file = 'icons/mob/inhands/items_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items_righthand.dmi'
clothing_traits = list(TRAIT_NEARSIGHTED_CORRECTED)
/obj/item/clothing/glasses/material
name = "optical material scanner"

View File

@@ -18,10 +18,8 @@
/datum/loadout_item/accessory/get_ui_buttons()
if(!can_be_layer_adjusted)
return ..()
var/list/buttons = ..()
UNTYPED_LIST_ADD(buttons, list(
. = ..()
UNTYPED_LIST_ADD(., list(
"label" = "Layer",
"act_key" = "set_layer",
"active_key" = INFO_LAYER,
@@ -29,7 +27,7 @@
"inactive_text" = "Below Suit",
))
return buttons
return .
/datum/loadout_item/accessory/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params)
if(action == "set_layer")
@@ -84,7 +82,6 @@
/datum/loadout_item/accessory/full_pocket_protector
name = "Pocket Protector (Filled)"
item_path = /obj/item/clothing/accessory/pocketprotector/full
additional_displayed_text = list("Contains pens")
/datum/loadout_item/accessory/pride
name = "Pride Pin"

View File

@@ -13,20 +13,19 @@
LAZYADD(outfit.backpack_contents, outfit.glasses)
outfit.glasses = item_path
/datum/loadout_item/glasses/prescription_glasses
/datum/loadout_item/glasses/regular
name = "Glasses"
item_path = /obj/item/clothing/glasses/regular
additional_displayed_text = list("Prescription")
/datum/loadout_item/glasses/prescription_glasses/circle_glasses
/datum/loadout_item/glasses/circle_glasses
name = "Circle Glasses"
item_path = /obj/item/clothing/glasses/regular/circle
/datum/loadout_item/glasses/prescription_glasses/hipster_glasses
/datum/loadout_item/glasses/hipster_glasses
name = "Hipster Glasses"
item_path = /obj/item/clothing/glasses/regular/hipster
/datum/loadout_item/glasses/prescription_glasses/jamjar_glasses
/datum/loadout_item/glasses/jamjar_glasses
name = "Jamjar Glasses"
item_path = /obj/item/clothing/glasses/regular/jamjar
@@ -58,7 +57,7 @@
name = "Medical Eyepatch"
item_path = /obj/item/clothing/glasses/eyepatch/medical
/datum/loadout_item/glasses/prescription_glasses/kim
/datum/loadout_item/glasses/kim
name = "Thin Glasses"
item_path = /obj/item/clothing/glasses/regular/kim

View File

@@ -155,4 +155,3 @@
/datum/loadout_item/head/wig
name = "Natural Wig"
item_path = /obj/item/clothing/head/wig/natural
additional_displayed_text = list("Hair Color")

View File

@@ -43,10 +43,144 @@
return ..()
/datum/loadout_item/pocket_items/plush
group = "Plushies"
abstract_type = /datum/loadout_item/pocket_items/plush
can_be_named = TRUE
/datum/loadout_item/pocket_items/plush/bee
name = "Plush (Bee)"
item_path = /obj/item/toy/plush/beeplushie
/datum/loadout_item/pocket_items/plush/carp
name = "Plush (Carp)"
ui_icon = 'icons/obj/fluff/previews.dmi'
ui_icon_state = "plushie_carp"
item_path = /obj/item/toy/plush/carpplushie
/datum/loadout_item/pocket_items/plush/lizard_greyscale
name = "Plush (Lizard, Colorable)"
ui_icon = 'icons/obj/fluff/previews.dmi'
ui_icon_state = "plushie_lizard"
item_path = /obj/item/toy/plush/lizard_plushie/greyscale
/datum/loadout_item/pocket_items/plush/lizard_random
name = "Plush (Lizard, Random)"
can_be_greyscale = DONT_GREYSCALE
ui_icon = 'icons/obj/fluff/previews.dmi'
ui_icon_state = "plushie_lizard_random"
item_path = /obj/item/toy/plush/lizard_plushie
/datum/loadout_item/pocket_items/plush/moth
name = "Plush (Moth)"
item_path = /obj/item/toy/plush/moth
/datum/loadout_item/pocket_items/plush/nukie
name = "Plush (Nukie)"
item_path = /obj/item/toy/plush/nukeplushie
/datum/loadout_item/pocket_items/plush/peacekeeper
name = "Plush (Peacekeeper)"
item_path = /obj/item/toy/plush/pkplush
/datum/loadout_item/pocket_items/plush/plasmaman
name = "Plush (Plasmaman)"
item_path = /obj/item/toy/plush/plasmamanplushie
/datum/loadout_item/pocket_items/plush/human
name = "Plush (human)"
item_path = /obj/item/toy/plush/human
/datum/loadout_item/pocket_items/plush/rouny
name = "Plush (Rouny)"
item_path = /obj/item/toy/plush/rouny
/datum/loadout_item/pocket_items/plush/snake
name = "Plush (Snake)"
ui_icon = 'icons/obj/fluff/previews.dmi'
ui_icon_state = "plushie_snake"
item_path = /obj/item/toy/plush/snakeplushie
/datum/loadout_item/pocket_items/dice
group = "Dice"
abstract_type = /datum/loadout_item/pocket_items/dice
/datum/loadout_item/pocket_items/dice/dice_bag
name = "Dice Bag"
item_path = /obj/item/storage/dice
/datum/loadout_item/pocket_items/dice/d1
name = "D1"
item_path = /obj/item/dice/d1
/datum/loadout_item/pocket_items/dice/d2
name = "D2"
item_path = /obj/item/dice/d2
/datum/loadout_item/pocket_items/dice/d4
name = "D4"
item_path = /obj/item/dice/d4
/datum/loadout_item/pocket_items/dice/d6
name = "D6"
item_path = /obj/item/dice/d6
/datum/loadout_item/pocket_items/dice/d6_ebony
name = "D6 (Ebony)"
item_path = /obj/item/dice/d6/ebony
/datum/loadout_item/pocket_items/dice/d6_space
name = "D6 (Space)"
item_path = /obj/item/dice/d6/space
/datum/loadout_item/pocket_items/dice/d8
name = "D8"
item_path = /obj/item/dice/d8
/datum/loadout_item/pocket_items/dice/d10
name = "D10"
item_path = /obj/item/dice/d10
/datum/loadout_item/pocket_items/dice/d12
name = "D12"
item_path = /obj/item/dice/d12
/datum/loadout_item/pocket_items/dice/d20
name = "D20"
item_path = /obj/item/dice/d20
/datum/loadout_item/pocket_items/dice/d100
name = "D100"
item_path = /obj/item/dice/d100
/datum/loadout_item/pocket_items/dice/d00
name = "D00"
item_path = /obj/item/dice/d00
/datum/loadout_item/pocket_items/card_binder
name = "Card Binder"
item_path = /obj/item/storage/card_binder
/datum/loadout_item/pocket_items/card_deck
name = "Playing Card Deck"
item_path = /obj/item/toy/cards/deck
/datum/loadout_item/pocket_items/kotahi_deck
name = "Kotahi Deck"
item_path = /obj/item/toy/cards/deck/kotahi
/datum/loadout_item/pocket_items/wizoff_deck
name = "Wizoff Deck"
item_path = /obj/item/toy/cards/deck/wizoff
/datum/loadout_item/pocket_items/lipstick
name = "Lipstick"
item_path = /obj/item/lipstick
additional_displayed_text = list("Recolorable")
/datum/loadout_item/pocket_items/lipstick/get_item_information()
. = ..()
.[FA_ICON_PALETTE] = "Recolorable"
/datum/loadout_item/pocket_items/lipstick/on_equip_item(
obj/item/lipstick/equipped_item,
@@ -87,6 +221,8 @@
"active_key" = INFO_GREYSCALE,
))
return .
/datum/loadout_item/pocket_items/lipstick/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params)
switch(action)
if("select_lipstick_style")
@@ -111,124 +247,6 @@
return ..()
/datum/loadout_item/pocket_items/plush
abstract_type = /datum/loadout_item/pocket_items/plush
can_be_named = TRUE
/datum/loadout_item/pocket_items/plush/bee
name = "Plush (Bee)"
item_path = /obj/item/toy/plush/beeplushie
/datum/loadout_item/pocket_items/plush/carp
name = "Plush (Carp)"
item_path = /obj/item/toy/plush/carpplushie
/datum/loadout_item/pocket_items/plush/lizard_greyscale
name = "Plush (Lizard, Colorable)"
item_path = /obj/item/toy/plush/lizard_plushie/greyscale
/datum/loadout_item/pocket_items/plush/lizard_random
name = "Plush (Lizard, Random)"
can_be_greyscale = DONT_GREYSCALE
item_path = /obj/item/toy/plush/lizard_plushie
additional_displayed_text = list("Random color")
/datum/loadout_item/pocket_items/plush/moth
name = "Plush (Moth)"
item_path = /obj/item/toy/plush/moth
/datum/loadout_item/pocket_items/plush/nukie
name = "Plush (Nukie)"
item_path = /obj/item/toy/plush/nukeplushie
/datum/loadout_item/pocket_items/plush/peacekeeper
name = "Plush (Peacekeeper)"
item_path = /obj/item/toy/plush/pkplush
/datum/loadout_item/pocket_items/plush/plasmaman
name = "Plush (Plasmaman)"
item_path = /obj/item/toy/plush/plasmamanplushie
/datum/loadout_item/pocket_items/plush/human
name = "Plush (human)"
item_path = /obj/item/toy/plush/human
/datum/loadout_item/pocket_items/plush/rouny
name = "Plush (Rouny)"
item_path = /obj/item/toy/plush/rouny
/datum/loadout_item/pocket_items/plush/snake
name = "Plush (Snake)"
item_path = /obj/item/toy/plush/snakeplushie
/datum/loadout_item/pocket_items/card_binder
name = "Card Binder"
item_path = /obj/item/storage/card_binder
/datum/loadout_item/pocket_items/card_deck
name = "Playing Card Deck"
item_path = /obj/item/toy/cards/deck
/datum/loadout_item/pocket_items/kotahi_deck
name = "Kotahi Deck"
item_path = /obj/item/toy/cards/deck/kotahi
/datum/loadout_item/pocket_items/wizoff_deck
name = "Wizoff Deck"
item_path = /obj/item/toy/cards/deck/wizoff
/datum/loadout_item/pocket_items/dice_bag
name = "Dice Bag"
item_path = /obj/item/storage/dice
/datum/loadout_item/pocket_items/d1
name = "D1"
item_path = /obj/item/dice/d1
/datum/loadout_item/pocket_items/d2
name = "D2"
item_path = /obj/item/dice/d2
/datum/loadout_item/pocket_items/d4
name = "D4"
item_path = /obj/item/dice/d4
/datum/loadout_item/pocket_items/d6
name = "D6"
item_path = /obj/item/dice/d6
/datum/loadout_item/pocket_items/d6_ebony
name = "D6 (Ebony)"
item_path = /obj/item/dice/d6/ebony
/datum/loadout_item/pocket_items/d6_space
name = "D6 (Space)"
item_path = /obj/item/dice/d6/space
/datum/loadout_item/pocket_items/d8
name = "D8"
item_path = /obj/item/dice/d8
/datum/loadout_item/pocket_items/d10
name = "D10"
item_path = /obj/item/dice/d10
/datum/loadout_item/pocket_items/d12
name = "D12"
item_path = /obj/item/dice/d12
/datum/loadout_item/pocket_items/d20
name = "D20"
item_path = /obj/item/dice/d20
/datum/loadout_item/pocket_items/d100
name = "D100"
item_path = /obj/item/dice/d100
/datum/loadout_item/pocket_items/d00
name = "D00"
item_path = /obj/item/dice/d00
/datum/loadout_item/pocket_items/lighter
name = "Zippo Lighter"
item_path = /obj/item/lighter

View File

@@ -35,6 +35,9 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories())
/// Displayed name of the loadout item.
/// Defaults to the item's name if unset.
var/name
/// Title of a group that this item will be bundled under
/// Defaults to parent category's title if unset
var/group = null
/// Whether this item has greyscale support.
/// Only works if the item is compatible with the GAGS system of coloring.
/// Set automatically to TRUE for all items that have the flag [IS_PLAYER_COLORABLE_1].
@@ -50,8 +53,6 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories())
var/abstract_type = /datum/loadout_item
/// The actual item path of the loadout item.
var/obj/item/item_path
/// Lazylist of additional "information" text to display about this item.
var/list/additional_displayed_text
/// Icon file (DMI) for the UI to use for preview icons.
/// Set automatically if null
var/ui_icon
@@ -285,9 +286,18 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories())
SHOULD_CALL_PARENT(TRUE)
var/list/formatted_item = list()
var/list/information = list()
var/list/fetched_info = get_item_information()
for (var/icon_name in fetched_info)
information += list(list(
"icon" = icon_name,
"tooltip" = fetched_info[icon_name]
))
formatted_item["name"] = name
formatted_item["group"] = group || category.category_name
formatted_item["path"] = item_path
formatted_item["information"] = get_item_information()
formatted_item["information"] = information
formatted_item["buttons"] = get_ui_buttons()
formatted_item["reskins"] = get_reskin_options()
formatted_item["icon"] = ui_icon
@@ -296,24 +306,18 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories())
/**
* Returns a list of information to display about this item in the loadout UI.
*
* These should be short strings, sub 14 characters generally.
* Icon -> tooltip displayed when its hovered over
*/
/datum/loadout_item/proc/get_item_information() as /list
SHOULD_CALL_PARENT(TRUE)
// Mothblocks is hellbent on recolorable and reskinnable being only tooltips for items for visual clarity, so ask her before changing these
var/list/displayed_text = list()
displayed_text += (additional_displayed_text || list())
if(can_be_greyscale)
displayed_text += "Recolorable"
if(can_be_named)
displayed_text += "Renamable"
displayed_text[FA_ICON_PALETTE] = "Recolorable"
if(can_be_reskinned)
displayed_text += "Reskinnable"
displayed_text[FA_ICON_SWATCHBOOK] = "Reskinnable"
return displayed_text

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -1,15 +1,15 @@
import { useBackend } from 'tgui/backend';
import {
Box,
Button,
DmIcon,
Flex,
Icon,
ImageButton,
NoticeBox,
Stack,
Tooltip,
} from 'tgui-core/components';
import { createSearch } from 'tgui-core/string';
import { LoadoutCategory, LoadoutItem, LoadoutManagerData } from './base';
import type { LoadoutCategory, LoadoutItem, LoadoutManagerData } from './base';
type Props = {
item: LoadoutItem;
@@ -58,44 +58,44 @@ export function ItemDisplay(props: DisplayProps) {
const { act } = useBackend();
const { active, item, scale = 3 } = props;
const boxSize = `${scale * 32}px`;
return (
<Button
height={boxSize}
width={boxSize}
<div style={{ position: 'relative' }}>
<ImageButton
imageSize={scale * 32}
color={active ? 'green' : 'default'}
style={{ textTransform: 'capitalize', zIndex: '1' }}
tooltip={item.name}
tooltipPosition={'bottom'}
dmIcon={item.icon}
dmIconState={item.icon_state}
onClick={() =>
act('select_item', {
path: item.path,
deselect: active,
})
}
/>
<div
style={{ position: 'absolute', top: '8px', right: '8px', zIndex: '2' }}
>
<Flex vertical>
<Flex.Item>
<ItemIcon item={item} scale={scale} />
</Flex.Item>
{item.information.length > 0 && (
<Flex.Item ml={-5.5} style={{ zIndex: '3' }}>
<Stack vertical>
{item.information.map((info) => (
<Box
height="9px"
key={info}
fontSize="9px"
<Stack.Item
key={info.icon}
fontSize="14px"
textColor={'darkgray'}
bold
>
{info}
</Box>
<Tooltip position="right" content={info.tooltip}>
<Icon name={info.icon} />
</Tooltip>
</Stack.Item>
))}
</Flex.Item>
</Stack>
)}
</Flex>
</Button>
</div>
</div>
);
}
@@ -103,21 +103,66 @@ type ListProps = {
items: LoadoutItem[];
};
type LoadoutGroup = {
items: LoadoutItem[];
title: string;
};
function sortByGroup(items: LoadoutItem[]): LoadoutGroup[] {
const groups: LoadoutGroup[] = [];
for (let i = 0; i < items.length; i++) {
const item: LoadoutItem = items[i];
let usedGroup: LoadoutGroup | undefined = groups.find(
(group) => group.title === item.group,
);
if (usedGroup === undefined) {
usedGroup = { items: [], title: item.group };
groups.push(usedGroup);
}
usedGroup.items.push(item);
}
return groups;
}
export function ItemListDisplay(props: ListProps) {
const { data } = useBackend<LoadoutManagerData>();
const { loadout_list } = data.character_preferences.misc;
const itemGroups = sortByGroup(props.items);
return (
<Flex wrap>
{props.items.map((item) => (
<Flex.Item key={item.name} mr={2} mb={2}>
<Stack vertical>
{itemGroups.length > 1 && <Stack.Item />}
{itemGroups.map((group) => (
<Stack.Item key={group.title}>
<Stack vertical>
{itemGroups.length > 1 && (
<>
<Stack.Item mt={-1.5} mb={-0.8} ml={1.5}>
<h3 color="grey">{group.title}</h3>
</Stack.Item>
<Stack.Divider />
</>
)}
<Stack.Item>
<Stack wrap g={0.5}>
{group.items.map((item) => (
<Stack.Item key={item.name}>
<ItemDisplay
item={item}
active={loadout_list && loadout_list[item.path] !== undefined}
active={
loadout_list && loadout_list[item.path] !== undefined
}
/>
</Flex.Item>
</Stack.Item>
))}
</Flex>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
))}
</Stack>
);
}

View File

@@ -1,7 +1,7 @@
import { BooleanLike } from 'tgui-core/react';
import type { BooleanLike } from 'tgui-core/react';
import { PreferencesMenuData } from '../../types';
import { LoadoutButton } from './ModifyPanel';
import type { PreferencesMenuData } from '../../types';
import type { LoadoutButton } from './ModifyPanel';
// Generic types
export type DmIconFile = string;
@@ -23,15 +23,21 @@ export type ReskinOption = {
skin_icon_state: DmIconState; // The icon is the same as the item icon
};
export type LoadoutTooltip = {
icon: string;
tooltip: string;
};
// Actual item passed in from the loadout
export type LoadoutItem = {
name: string;
group: string;
path: typePath;
icon: DmIconFile | null;
icon_state: DmIconState | null;
buttons: LoadoutButton[];
reskins: ReskinOption[] | null;
information: string[];
information: LoadoutTooltip[];
};
// Category of items in the loadout

View File

@@ -14,7 +14,7 @@ import {
} from 'tgui-core/components';
import { useServerPrefs } from '../../useServerPrefs';
import {
import type {
LoadoutCategory,
LoadoutItem,
LoadoutManagerData,
@@ -136,7 +136,7 @@ function LoadoutTabs(props: LoadoutTabsProps) {
<Stack.Item grow>
{searching || activeCategory?.contents ? (
<Section
title={searching ? 'Searching...' : 'Catalog'}
title={searching ? 'Search results' : 'Catalog'}
fill
scrollable
buttons={