diff --git a/code/modules/client/loadout_picker.dm b/code/modules/client/loadout_picker.dm
new file mode 100644
index 000000000000..b535a5deb1d0
--- /dev/null
+++ b/code/modules/client/loadout_picker.dm
@@ -0,0 +1,89 @@
+/datum/loadout_picker/ui_static_data(mob/user)
+ . = ..()
+
+ .["categories"] = list()
+ for(var/category in GLOB.gear_datums_by_category)
+ var/list/datum/gear/gears = GLOB.gear_datums_by_category[category]
+
+ var/items = list()
+
+ for(var/gear_key as anything in gears)
+ var/datum/gear/gear = gears[gear_key]
+ items += list(
+ list("name" = gear.display_name, "cost" = gear.cost, "icon" = gear.path::icon, "icon_state" = gear.path::icon_state)
+ )
+
+ .["categories"] += list(
+ list("name" = category, "items" = items)
+ )
+
+ .["max_points"] = MAX_GEAR_COST
+
+/datum/loadout_picker/ui_data(mob/user)
+ . = ..()
+
+ var/datum/preferences/prefs = user.client?.prefs
+ if(!prefs)
+ return
+
+ var/points = 0
+
+ .["loadout"] = list()
+
+ for(var/item in prefs.gear)
+ var/datum/gear/gear = GLOB.gear_datums_by_name[item]
+ points += gear.cost
+
+ .["loadout"] += list(
+ list("name" = gear.display_name, "cost" = gear.cost, "icon" = gear.path::icon, "icon_state" = gear.path::icon_state)
+ )
+
+ .["points"] = points
+
+/datum/loadout_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+
+ var/datum/preferences/prefs = ui.user.client?.prefs
+ if(!prefs)
+ return
+
+ switch(action)
+ if("add")
+ var/datum/gear/gear = GLOB.gear_datums_by_name[params["name"]]
+ if(!istype(gear))
+ return
+
+ var/total_cost = 0
+ for(var/gear_name in prefs.gear)
+ total_cost += GLOB.gear_datums_by_name[gear_name].cost
+
+ total_cost += gear.cost
+ if(total_cost > MAX_GEAR_COST)
+ return
+
+ prefs.gear += gear.display_name
+
+ if("remove")
+ var/datum/gear/gear = GLOB.gear_datums_by_name[params["name"]]
+ if(!istype(gear))
+ return
+
+ prefs.gear -= gear.display_name
+
+ prefs.ShowChoices(ui.user)
+ return TRUE
+
+/datum/loadout_picker/tgui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+
+ ui = SStgui.try_update_ui(user, src, ui)
+
+ if(!ui)
+ ui = new(user, src, "LoadoutPicker", "Loadout Picker")
+ ui.open()
+ ui.set_autoupdate(FALSE)
+
+ winset(user, ui.window.id, "focus=true")
+
+/datum/loadout_picker/ui_state(mob/user)
+ return GLOB.always_state
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 5096ebc7dd53..9f7f2b5338ea 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -32,6 +32,7 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/static/datum/hair_picker/hair_picker = new
var/static/datum/body_picker/body_picker = new
+ var/static/datum/loadout_picker/loadout_picker = new
//doohickeys for savefiles
var/path
@@ -396,7 +397,7 @@ GLOBAL_LIST_INIT(bgstate_options, list(
dat += "Preferred Armor: [preferred_armor]
"
dat += "Show Job Gear: [show_job_gear ? "True" : "False"]
"
- dat += "Background: Cycle Background
"
+ dat += "Background: Cycle Background
"
dat += "Custom Loadout: "
var/total_cost = 0
@@ -410,16 +411,13 @@ GLOBAL_LIST_INIT(bgstate_options, list(
var/datum/gear/G = GLOB.gear_datums_by_name[gear[i]]
if(G)
total_cost += G.cost
- dat += "[gear[i]] ([G.cost] points) Remove
"
+ dat += "[gear[i]] ([G.cost] points)
"
dat += "Used: [total_cost] points"
else
dat += "None"
- if(total_cost < MAX_GEAR_COST)
- dat += " Add"
- if(LAZYLEN(gear))
- dat += " Clear"
+ dat += "
Open Loadout"
dat += ""
@@ -1033,39 +1031,8 @@ GLOBAL_LIST_INIT(bgstate_options, list(
set_job_slots(user)
return TRUE
if("loadout")
- switch(href_list["task"])
- if("input")
- var/gear_category = tgui_input_list(user, "Select gear category: ", "Gear to add", GLOB.gear_datums_by_category)
- if(!gear_category)
- return
- var/choice = tgui_input_list(user, "Select gear to add: ", gear_category, GLOB.gear_datums_by_category[gear_category])
- if(!choice)
- return
-
- var/total_cost = 0
- var/datum/gear/G
- if(isnull(gear) || !islist(gear))
- gear = list()
- if(length(gear))
- for(var/gear_name in gear)
- G = GLOB.gear_datums_by_name[gear_name]
- total_cost += G?.cost
-
- G = GLOB.gear_datums_by_category[gear_category][choice]
- total_cost += G.cost
- if(total_cost <= MAX_GEAR_COST)
- gear += G.display_name
- to_chat(user, SPAN_NOTICE("Added \the '[G.display_name]' for [G.cost] points ([MAX_GEAR_COST - total_cost] points remaining)."))
- else
- to_chat(user, SPAN_WARNING("Adding \the '[choice]' will exceed the maximum loadout cost of [MAX_GEAR_COST] points."))
-
- if("remove")
- var/i_remove = text2num(href_list["gear"])
- if(i_remove < 1 || i_remove > length(gear)) return
- gear.Cut(i_remove, i_remove + 1)
-
- if("clear")
- gear.Cut()
+ loadout_picker.tgui_interact(user)
+ return
if("flavor_text")
switch(href_list["task"])
@@ -1938,13 +1905,7 @@ GLOBAL_LIST_INIT(bgstate_options, list(
load_character()
reload_cooldown = world.time + 50
- // Refresh pickers
- var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
- if(picker_ui)
- picker_ui.send_update()
- picker_ui = SStgui.get_open_ui(user, body_picker)
- if(picker_ui)
- picker_ui.send_update()
+ update_all_pickers(user)
if("open_load_dialog")
if(!IsGuestKey(user.key))
@@ -1962,13 +1923,7 @@ GLOBAL_LIST_INIT(bgstate_options, list(
if(istype(np))
np.new_player_panel_proc()
- // Refresh pickers
- var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
- if(picker_ui)
- picker_ui.send_update()
- picker_ui = SStgui.get_open_ui(user, body_picker)
- if(picker_ui)
- picker_ui.send_update()
+ update_all_pickers(user)
if("tgui_fancy")
tgui_fancy = !tgui_fancy
@@ -2325,6 +2280,17 @@ GLOBAL_LIST_INIT(bgstate_options, list(
completed_tutorials = splittext(savestring, ";")
return completed_tutorials
+/// Refreshes all open TGUI interfaces inside the character prefs menu
+/datum/preferences/proc/update_all_pickers(mob/user)
+ var/datum/tgui/picker_ui = SStgui.get_open_ui(user, hair_picker)
+ picker_ui?.send_update()
+
+ picker_ui = SStgui.get_open_ui(user, body_picker)
+ picker_ui?.send_update()
+
+ picker_ui = SStgui.get_open_ui(user, loadout_picker)
+ picker_ui?.send_update()
+
#undef MENU_MARINE
#undef MENU_XENOMORPH
#undef MENU_CO
diff --git a/code/modules/client/preferences_gear.dm b/code/modules/client/preferences_gear.dm
index 6f649eb61ddb..dc35ba261b2c 100644
--- a/code/modules/client/preferences_gear.dm
+++ b/code/modules/client/preferences_gear.dm
@@ -1,5 +1,5 @@
GLOBAL_LIST_EMPTY(gear_datums_by_category)
-GLOBAL_LIST_EMPTY(gear_datums_by_name)
+GLOBAL_LIST_EMPTY_TYPED(gear_datums_by_name, /datum/gear)
/proc/populate_gear_list()
var/datum/gear/G
@@ -19,7 +19,7 @@ GLOBAL_LIST_EMPTY(gear_datums_by_name)
/datum/gear
var/display_name // Name/index.
var/category //Used for sorting in the loadout selection.
- var/path // Path to item.
+ var/obj/item/path // Path to item.
var/cost = 2 // Number of points used.
var/slot // Slot to equip to, if any.
var/list/allowed_roles // Roles that can spawn with this item.
diff --git a/colonialmarines.dme b/colonialmarines.dme
index bc20c8cef91b..ef502051b1b5 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -1564,6 +1564,7 @@
#include "code\modules\client\color_picker.dm"
#include "code\modules\client\country_flags.dm"
#include "code\modules\client\hair_picker.dm"
+#include "code\modules\client\loadout_picker.dm"
#include "code\modules\client\player_details.dm"
#include "code\modules\client\preferences.dm"
#include "code\modules\client\preferences_factions.dm"
diff --git a/tgui/packages/tgui/interfaces/LoadoutPicker.tsx b/tgui/packages/tgui/interfaces/LoadoutPicker.tsx
new file mode 100644
index 000000000000..08f7285bf2c5
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LoadoutPicker.tsx
@@ -0,0 +1,131 @@
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import { Box, Button, DmIcon, Section, Stack } from '../components';
+import { Window } from '../layouts';
+import { Loader } from './common/Loader';
+
+type LoadoutPickerData = {
+ categories: {
+ name: string;
+ items: LoadoutItem[];
+ }[];
+ points: number;
+ max_points: number;
+ loadout: LoadoutItem[];
+};
+
+type LoadoutItem = {
+ name: string;
+ cost: number;
+ icon: string;
+ icon_state: string;
+};
+
+export const LoadoutPicker = () => {
+ const { data } = useBackend();
+
+ const { categories, points, max_points, loadout } = data;
+
+ const [selected, setSelected] = useState(categories[0]);
+
+ return (
+
+
+
+
+
+
+
+
+ {categories.map((category) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ {loadout.map((item) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {selected.items.map((item) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+const ItemRender = (props: {
+ readonly item: LoadoutItem;
+ readonly loadout?: boolean;
+}) => {
+ const { item, loadout } = props;
+
+ const { icon, icon_state, name, cost } = item;
+
+ const { data, act } = useBackend();
+
+ const { points, max_points } = data;
+
+ const atLimit = points + cost > max_points;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/styles/interfaces/LoadoutPicker.scss b/tgui/packages/tgui/styles/interfaces/LoadoutPicker.scss
new file mode 100644
index 000000000000..4e77b3e9f1e7
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/LoadoutPicker.scss
@@ -0,0 +1,7 @@
+.theme-crtblue {
+ .LoadoutPicker {
+ .Stack--horizontal > .ItemPicker:first-of-type {
+ margin-left: 6px;
+ }
+ }
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index dbc514428cae..d8995a546f47 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -64,6 +64,7 @@
@include meta.load-css('./interfaces/ElevatorControl.scss');
@include meta.load-css('./interfaces/ExperimentConfigure.scss');
@include meta.load-css('./interfaces/HairPicker.scss');
+@include meta.load-css('./interfaces/LoadoutPicker.scss');
@include meta.load-css('./interfaces/MarkMenu.scss');
@include meta.load-css('./interfaces/MedalsViewer.scss');
@include meta.load-css('./interfaces/NavigationShuttle.scss');
diff --git a/tgui/packages/tgui/styles/themes/crt.scss b/tgui/packages/tgui/styles/themes/crt.scss
index 71043439b4cf..ba4264ddb1fb 100644
--- a/tgui/packages/tgui/styles/themes/crt.scss
+++ b/tgui/packages/tgui/styles/themes/crt.scss
@@ -165,7 +165,7 @@ $scrollbar-color-multiplier: 0.5 !default;
font-weight: bold;
}
- .Button {
+ .Button:not(.Button--selected) {
font-family: $font-family;
font-weight: bold;
border: 1px solid base.$color-fg;