From 1d112623a0b5e1e815a4cfb3c7ec8fde1628b54d Mon Sep 17 00:00:00 2001
From: Aylong <69762909+AyIong@users.noreply.github.com>
Date: Sun, 12 Jan 2025 04:47:08 +0200
Subject: [PATCH] Stack crafting images (#89020)
## About The Pull Request
- Stack crafting got images, and a redesign, as it now uses the
ImageButtons component instead of the usual buttons
- Search will no longer stop the character when opening the stack
crafting menu
- Icon into SearchBar component now centered
## Why It's Good For The Game
Images greatly simplify interactions with interfaces, especially those
where it is desirable to see what you are doing (crafting)
So... better UI/UX :high:
## Demostration

Video with almost all materials
https://github.com/user-attachments/assets/6e0e90b7-064a-48de-afe9-9e097abaa011
## Changelog
:cl:
qol: Stack crafting UI got images
qol: Search in stack crafting UI will not take control from you, feel
free to craft on walk
/:cl:
---------
Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
---
code/game/objects/items/stacks/stack.dm | 21 ++-
.../game/objects/items/stacks/stack_recipe.dm | 11 ++
.../tgui/interfaces/StackCrafting.tsx | 147 +++++++++++-------
.../tgui/interfaces/common/SearchBar.tsx | 4 +-
4 files changed, 122 insertions(+), 61 deletions(-)
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index 999fbd5608d..9310786755c 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -239,12 +239,21 @@
* * R - The stack recipe we are using to get a list of properties
*/
/obj/item/stack/proc/build_recipe(datum/stack_recipe/R)
- return list(
- "res_amount" = R.res_amount,
- "max_res_amount" = R.max_res_amount,
- "req_amount" = R.req_amount,
- "ref" = text_ref(R),
- )
+ var/list/data = list()
+ var/obj/result = R.result_type
+
+ data["ref"] = text_ref(R)
+ data["req_amount"] = R.req_amount
+ data["res_amount"] = R.res_amount
+ data["max_res_amount"] = R.max_res_amount
+ data["icon"] = result.icon
+ data["icon_state"] = result.icon_state
+
+ // DmIcon cannot paint images. So, if we have grayscale sprite, we need ready base64 image.
+ if(R.result_image)
+ data["image"] = R.result_image
+
+ return data
/**
* Checks if the recipe is valid to be used
diff --git a/code/game/objects/items/stacks/stack_recipe.dm b/code/game/objects/items/stacks/stack_recipe.dm
index a11feee8c68..11d5315e5f6 100644
--- a/code/game/objects/items/stacks/stack_recipe.dm
+++ b/code/game/objects/items/stacks/stack_recipe.dm
@@ -7,6 +7,8 @@
var/title = "ERROR"
/// What atom the recipe makes, typepath
var/atom/result_type
+ /// Generated base64 image. Used only if result has color
+ var/result_image
/// Amount of stack required to make
var/req_amount = 1
/// Amount of resulting atoms made
@@ -53,6 +55,15 @@
src.trait_modifier = trait_modifier
src.category = src.category || category || CAT_MISC
+ // We create base64 image only if item have color. Otherwise use icon_ref for TGUI
+ var/obj/item/result = result_type
+ var/paint = result::color
+ if(!isnull(paint) && paint != COLOR_WHITE)
+ var/icon/result_icon = icon(result::icon, result::icon_state, SOUTH, 1)
+ result_icon.Scale(32, 32)
+ result_icon.Blend(paint, ICON_MULTIPLY)
+ src.result_image = "[icon2base64(result_icon)]"
+
/datum/stack_recipe/radial
/// Optional info to be shown on the radial option for this item
var/desc
diff --git a/tgui/packages/tgui/interfaces/StackCrafting.tsx b/tgui/packages/tgui/interfaces/StackCrafting.tsx
index 38a7cbdf24c..b45d24e861e 100644
--- a/tgui/packages/tgui/interfaces/StackCrafting.tsx
+++ b/tgui/packages/tgui/interfaces/StackCrafting.tsx
@@ -1,22 +1,25 @@
import { useState } from 'react';
import {
- Box,
Button,
Collapsible,
- Input,
+ ImageButton,
NoticeBox,
Section,
- Table,
+ Stack,
} from 'tgui-core/components';
import { clamp } from 'tgui-core/math';
-import { createSearch } from 'tgui-core/string';
+import { createSearch, toTitleCase } from 'tgui-core/string';
import { useBackend } from '../backend';
import { Window } from '../layouts';
+import { SearchBar } from './common/SearchBar';
type Recipe = {
ref: unknown | null;
req_amount: number;
+ icon: string;
+ icon_state: string;
+ image?: string;
// for multiplier buttons
res_amount: number;
@@ -88,7 +91,16 @@ const filterRecipeList = (
return [];
})
- .sort(([a], [b]) => (a < b ? -1 : a !== b ? 1 : 0)),
+
+ // Sort items so that lists are on top and recipes underneath.
+ // Plus everything is in alphabetical order.
+ .sort(([aKey, aValue], [bKey, bValue]) =>
+ isRecipeList(aValue) !== isRecipeList(bValue)
+ ? isRecipeList(aValue)
+ ? -1
+ : 1
+ : aKey.localeCompare(bKey),
+ ),
);
return Object.keys(filteredList).length ? filteredList : undefined;
@@ -102,23 +114,21 @@ export const StackCrafting = (_props) => {
const testSearch = createSearch(searchText, (item: string) => item);
const filteredRecipes = filterRecipeList(recipes, testSearch);
- const height: number = clamp(94 + Object.keys(recipes).length * 26, 250, 500);
+ const height: number = clamp(96 + Object.keys(recipes).length * 37, 250, 500);
return (
-
+
- Search
- setSearchText(value)}
- mx={1}
- />
- >
+ setSearchText(value)}
+ />
}
>
{filteredRecipes ? (
@@ -141,10 +151,19 @@ const RecipeListBox = (props: RecipeListProps) => {
const recipe = recipes[title];
if (isRecipeList(recipe)) {
return (
-
-
-
-
+
+
);
} else {
@@ -181,14 +200,19 @@ const Multipliers = (props: MultiplierProps) => {
if (maxM >= multiplier) {
finalResult.push(
,
);
}
}
@@ -196,14 +220,19 @@ const Multipliers = (props: MultiplierProps) => {
if (multipliers.indexOf(maxM) === -1) {
finalResult.push(
,
);
}
@@ -212,44 +241,54 @@ const Multipliers = (props: MultiplierProps) => {
const RecipeBox = (props: RecipeBoxProps) => {
const { act, data } = useBackend();
-
const { amount } = data;
-
const { recipe, title } = props;
-
- const { res_amount, max_res_amount, req_amount, ref } = recipe;
+ const {
+ res_amount,
+ max_res_amount,
+ req_amount,
+ ref,
+ icon,
+ icon_state,
+ image,
+ } = recipe;
const resAmountLabel = res_amount > 1 ? `${res_amount}x ` : '';
const sheetSuffix = req_amount > 1 ? 's' : '';
- const buttonName = `${resAmountLabel}${title} (${req_amount} sheet${sheetSuffix})`;
+ const buttonName = `${resAmountLabel}${title}`;
+ const reqSheets = `${req_amount} sheet${sheetSuffix}`;
const maxMultiplier = buildMultiplier(recipe, amount);
return (
-
-
-
-
-
- {max_res_amount > 1 && maxMultiplier > 1 && (
-
-
-
- )}
-
-
-
+ 1 &&
+ maxMultiplier > 1 && (
+
+ )
+ }
+ onClick={() =>
+ act('make', {
+ ref: ref,
+ multiplier: 1,
+ })
+ }
+ >
+
+ {toTitleCase(buttonName)}
+
+ {reqSheets}
+
+
+
);
};
diff --git a/tgui/packages/tgui/interfaces/common/SearchBar.tsx b/tgui/packages/tgui/interfaces/common/SearchBar.tsx
index e6a52eb7abc..27b6f68a13a 100644
--- a/tgui/packages/tgui/interfaces/common/SearchBar.tsx
+++ b/tgui/packages/tgui/interfaces/common/SearchBar.tsx
@@ -37,7 +37,9 @@ export function SearchBar(props: Props) {
return (
- {!noIcon && }
+
+ {!noIcon && }
+