From d244c86ce64a7fca21a9d28dd002620deb67179b Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Tue, 11 Jun 2024 19:50:12 -0500 Subject: [PATCH] Adds Character Loadout Tab to preferences (with just a small handful of items to start) (#83521) ## About The Pull Request Adds a Character Loadout Tab to the preference menu This tab lets you pick items to start the round with ![image](https://private-user-images.githubusercontent.com/51863163/336254447-c5f7eefa-c44c-418d-b48e-0409bb5bb982.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTgwNDAxMjMsIm5iZiI6MTcxODAzOTgyMywicGF0aCI6Ii81MTg2MzE2My8zMzYyNTQ0NDctYzVmN2VlZmEtYzQ0Yy00MThkLWI0OGUtMDQwOWJiNWJiOTgyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjEwVDE3MTcwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWYxYWFmYjI2NDU0YjUyODg3NjBmM2VjZDg4YWQ1YjlhMThmODU3MDYyMzYwOGVmYTcxYmY2MDhjZWVmYjQ5ZTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.Y0_19Gisfp4yyUmLgW2atfKyneL7POWFRKNVgNWTbEs) This also has some additional mechanics, such as being able to recolor colorable items, rename certain items (such as plushies), set item skins (such as the pride pin) ![image](https://github.com/tgstation/tgstation/assets/51863163/8a085d6c-a294-4538-95d2-ada902ab69b4) ## Why It's Good For The Game This has been headcoder sanctioned for some time, just no one did it. So here we are. Allows players to add some additional customization to their characters. Keeps us from cluttering the quirks list with quirks that do nothing but grants items. ## Changelog :cl: Melbert add: Character Loadouts del: Pride Pin quirk (it's in the Loadout menu now) /:cl: --- code/__DEFINES/preferences.dm | 14 + code/__DEFINES/traits/declarations.dm | 3 + code/_globalvars/traits/_traits.dm | 1 + code/controllers/subsystem/job.dm | 2 +- .../subsystem/processing/quirks.dm | 2 +- .../datums/quirks/neutral_quirks/pride_pin.dm | 19 - code/game/gamemodes/objective.dm | 5 +- code/game/gamemodes/objective_items.dm | 16 +- code/game/objects/items/plushes.dm | 5 + code/modules/admin/verbs/mapping.dm | 6 +- code/modules/antagonists/spy/spy_bounty.dm | 14 +- .../antagonists/traitor/objectives/steal.dm | 4 + code/modules/client/preferences.dm | 11 +- .../modules/client/preferences/_preference.dm | 9 +- .../migrations/quirk_loadout_migration.dm | 27 ++ code/modules/client/preferences/pride_pin.dm | 16 - code/modules/client/preferences_savefile.dm | 9 +- code/modules/clothing/head/hat.dm | 2 + code/modules/clothing/head/jobs.dm | 4 +- code/modules/clothing/neck/_neck.dm | 2 + .../clothing/under/accessories/badges.dm | 23 +- code/modules/jobs/job_types/_job.dm | 19 +- .../jobs/job_types/assistant/assistant.dm | 4 +- .../modules/loadout/categories/accessories.dm | 92 +++++ code/modules/loadout/categories/glasses.dm | 55 +++ code/modules/loadout/categories/heads.dm | 138 +++++++ code/modules/loadout/categories/inhands.dm | 33 ++ code/modules/loadout/categories/neck.dm | 36 ++ code/modules/loadout/categories/pocket.dm | 203 ++++++++++ code/modules/loadout/loadout_categories.dm | 79 ++++ code/modules/loadout/loadout_helpers.dm | 80 ++++ code/modules/loadout/loadout_items.dm | 371 ++++++++++++++++++ code/modules/loadout/loadout_menu.dm | 120 ++++++ code/modules/loadout/loadout_preference.dm | 59 +++ .../mob/dead/new_player/preferences_setup.dm | 15 +- icons/obj/fluff/previews.dmi | Bin 7799 -> 8491 bytes tgstation.dme | 14 +- .../CharacterPreferenceWindow.tsx | 17 + .../tgui/interfaces/PreferencesMenu/data.ts | 6 + .../PreferencesMenu/loadout/ItemDisplay.tsx | 147 +++++++ .../PreferencesMenu/loadout/ModifyPanel.tsx | 219 +++++++++++ .../PreferencesMenu/loadout/base.ts | 46 +++ .../PreferencesMenu/loadout/index.tsx | 337 ++++++++++++++++ .../character_preferences/pride_pin.tsx | 7 - 44 files changed, 2192 insertions(+), 99 deletions(-) delete mode 100644 code/datums/quirks/neutral_quirks/pride_pin.dm create mode 100644 code/modules/client/preferences/migrations/quirk_loadout_migration.dm delete mode 100644 code/modules/client/preferences/pride_pin.dm create mode 100644 code/modules/loadout/categories/accessories.dm create mode 100644 code/modules/loadout/categories/glasses.dm create mode 100644 code/modules/loadout/categories/heads.dm create mode 100644 code/modules/loadout/categories/inhands.dm create mode 100644 code/modules/loadout/categories/neck.dm create mode 100644 code/modules/loadout/categories/pocket.dm create mode 100644 code/modules/loadout/loadout_categories.dm create mode 100644 code/modules/loadout/loadout_helpers.dm create mode 100644 code/modules/loadout/loadout_items.dm create mode 100644 code/modules/loadout/loadout_menu.dm create mode 100644 code/modules/loadout/loadout_preference.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ItemDisplay.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ModifyPanel.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/loadout/base.ts create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/loadout/index.tsx delete mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pride_pin.tsx diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 2989310fb9b..51a995c90e1 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -142,3 +142,17 @@ /// The key used for sprite accessories that should never actually be applied to the player. #define SPRITE_ACCESSORY_NONE "None" + +// Loadout +/// Used to make something not recolorable even if it's capable +#define DONT_GREYSCALE -1 +// Loadout item info keys +// Changing these will break existing loadouts +/// Tracks GAGS color information +#define INFO_GREYSCALE "greyscale" +/// Used to set custom names +#define INFO_NAMED "name" +/// Used for specific alt-reskins, like the pride pin +#define INFO_RESKIN "reskin" +/// Handles which layer the item will be on, for accessories +#define INFO_LAYER "layer" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 457f7dfe451..17d9008a543 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1151,4 +1151,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Does this item bypass ranged armor checks? #define TRAIT_BYPASS_RANGED_ARMOR "bypass_ranged_armor" +/// This item cannot be selected for or used by a theft objective (Spies, Traitors, etc.) +#define TRAIT_ITEM_OBJECTIVE_BLOCKED "item_objective_blocked" + // END TRAIT DEFINES diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 94349d689de..1fe465da39f 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -522,6 +522,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_HAUNTED" = TRAIT_HAUNTED, "TRAIT_HONKSPAMMING" = TRAIT_HONKSPAMMING, "TRAIT_INNATELY_FANTASTICAL_ITEM" = TRAIT_INNATELY_FANTASTICAL_ITEM, + "TRAIT_ITEM_OBJECTIVE_BLOCKED" = TRAIT_ITEM_OBJECTIVE_BLOCKED, "TRAIT_NEEDS_TWO_HANDS" = TRAIT_NEEDS_TWO_HANDS, "TRAIT_NO_BARCODES" = TRAIT_NO_BARCODES, "TRAIT_NO_STORAGE_INSERT" = TRAIT_NO_STORAGE_INSERT, diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index 5f12370c60d..dc1091bcfc5 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -557,7 +557,7 @@ SUBSYSTEM_DEF(job) SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job) equipping.mind?.set_assigned_role_with_greeting(job, player_client) - equipping.on_job_equipping(job) + equipping.on_job_equipping(job, player_client) job.announce_job(equipping) if(player_client?.holder) diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index a915346cebd..bd782968abe 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -10,7 +10,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list( list(/datum/quirk/no_taste, /datum/quirk/vegetarian, /datum/quirk/deviant_tastes, /datum/quirk/gamer), list(/datum/quirk/pineapple_liker, /datum/quirk/pineapple_hater, /datum/quirk/gamer), list(/datum/quirk/alcohol_tolerance, /datum/quirk/light_drinker), - list(/datum/quirk/item_quirk/clown_enjoyer, /datum/quirk/item_quirk/mime_fan, /datum/quirk/item_quirk/pride_pin), + list(/datum/quirk/item_quirk/clown_enjoyer, /datum/quirk/item_quirk/mime_fan), list(/datum/quirk/bad_touch, /datum/quirk/friendly), list(/datum/quirk/extrovert, /datum/quirk/introvert), list(/datum/quirk/prosthetic_limb, /datum/quirk/quadruple_amputee, /datum/quirk/transhumanist, /datum/quirk/body_purist), diff --git a/code/datums/quirks/neutral_quirks/pride_pin.dm b/code/datums/quirks/neutral_quirks/pride_pin.dm deleted file mode 100644 index 488c0a2bccb..00000000000 --- a/code/datums/quirks/neutral_quirks/pride_pin.dm +++ /dev/null @@ -1,19 +0,0 @@ -/datum/quirk/item_quirk/pride_pin - name = "Pride Pin" - desc = "Show off your pride with this changing pride pin!" - icon = FA_ICON_RAINBOW - value = 0 - gain_text = span_notice("You feel fruity.") - lose_text = span_danger("You feel only slightly less fruity than before.") - medical_record_text = "Patient appears to be fruity." - -/datum/quirk/item_quirk/pride_pin/add_unique(client/client_source) - var/obj/item/clothing/accessory/pride/pin = new(get_turf(quirk_holder)) - - var/pride_choice = client_source?.prefs?.read_preference(/datum/preference/choiced/pride_pin) || assoc_to_keys(GLOB.pride_pin_reskins)[1] - var/pride_reskin = GLOB.pride_pin_reskins[pride_choice] - - pin.current_skin = pride_choice - pin.icon_state = pride_reskin - - give_item_to_holder(pin, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS)) diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index f0ca22b3b4f..88944035868 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -6,7 +6,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list var/name = "generic objective" //Name for admin prompts var/explanation_text = "Nothing" //What that person is supposed to do. ///if this objective doesn't print failure or success in the roundend report - var/no_failure = FALSE + var/no_failure = FALSE ///name used in printing this objective (Objective #1) var/objective_name = "Objective" var/team_explanation_text //For when there are multiple owners. @@ -688,6 +688,9 @@ GLOBAL_LIST_EMPTY(possible_items) var/list/all_items = M.current.get_all_contents() //this should get things in cheesewheels, books, etc. for(var/obj/I in all_items) //Check for items + if(HAS_TRAIT(I, TRAIT_ITEM_OBJECTIVE_BLOCKED)) + continue + if(istype(I, steal_target)) if(!targetinfo) //If there's no targetinfo, then that means it was a custom objective. At this point, we know you have the item, so return 1. return TRUE diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm index 8a994350be8..4bd635b7ebe 100644 --- a/code/game/gamemodes/objective_items.dm +++ b/code/game/gamemodes/objective_items.dm @@ -5,7 +5,7 @@ //Contains the target item datums for Steal objectives. /datum/objective_item /// How the item is described in the objective - var/name = "A silly bike horn! Honk!" + var/name = "a silly bike horn! Honk!" /// Typepath of item var/targetitem = /obj/item/bikehorn /// Valid containers that the target item can be in. @@ -566,7 +566,7 @@ // A number of special early-game steal objectives intended to be used with the steal-and-destroy objective. // They're basically items of utility or emotional value that may be found on many players or lying around the station. /datum/objective_item/steal/traitor/insuls - name = "insulated gloves" + name = "some insulated gloves" targetitem = /obj/item/clothing/gloves/color/yellow excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER) item_owner = list(JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER) @@ -578,7 +578,7 @@ return add_item_to_steal(src, /obj/item/clothing/gloves/color/yellow) /datum/objective_item/steal/traitor/moth_plush - name = "cute moth plush toy" + name = "a cute moth plush toy" targetitem = /obj/item/toy/plush/moth excludefromjob = list(JOB_PSYCHOLOGIST, JOB_PARAMEDIC, JOB_CHEMIST, JOB_MEDICAL_DOCTOR, JOB_CHIEF_MEDICAL_OFFICER, JOB_CORONER) exists_on_map = TRUE @@ -589,7 +589,7 @@ return add_item_to_steal(src, /obj/item/toy/plush/moth) /datum/objective_item/steal/traitor/lizard_plush - name = "cute lizard plush toy" + name = "a cute lizard plush toy" targetitem = /obj/item/toy/plush/lizard_plushie exists_on_map = TRUE difficulty = 1 @@ -633,7 +633,7 @@ return add_item_to_steal(src, /obj/item/book/manual/wiki/security_space_law) /datum/objective_item/steal/traitor/rpd - name = "rapid pipe dispenser" + name = "a rapid pipe dispenser" targetitem = /obj/item/pipe_dispenser excludefromjob = list( JOB_ATMOSPHERIC_TECHNICIAN, @@ -679,7 +679,7 @@ objective_type = OBJECTIVE_ITEM_TYPE_SPY /datum/objective_item/steal/spy/lamarr - name = "The Research Director's pet headcrab" + name = "the Research Director's pet headcrab" targetitem = /obj/item/clothing/mask/facehugger/lamarr excludefromjob = list(JOB_RESEARCH_DIRECTOR) exists_on_map = TRUE @@ -809,7 +809,7 @@ return add_item_to_steal(src, /obj/item/stamp/head) /datum/objective_item/steal/spy/sunglasses - name = "sunglasses" + name = "some sunglasses" targetitem = /obj/item/clothing/glasses/sunglasses excludefromjob = list( JOB_CAPTAIN, @@ -828,7 +828,7 @@ You can also obtain a pair from dissassembling hudglasses." /datum/objective_item/steal/spy/ce_modsuit - name = "the cheif engineer's advanced MOD control unit" + name = "the chief engineer's advanced MOD control unit" targetitem = /obj/item/mod/control/pre_equipped/advanced excludefromjob = list(JOB_CHIEF_ENGINEER) exists_on_map = TRUE diff --git a/code/game/objects/items/plushes.dm b/code/game/objects/items/plushes.dm index 1d5c37ffc22..052fc983c83 100644 --- a/code/game/objects/items/plushes.dm +++ b/code/game/objects/items/plushes.dm @@ -531,6 +531,11 @@ desc = "An adorable stuffed toy that resembles a green lizardperson. This one fills you with nostalgia and soul." greyscale_colors = "#66ff33#000000" +/obj/item/toy/plush/lizard_plushie/greyscale + desc = "An adorable stuffed toy that resembles a lizardperson. This one has been custom made." + greyscale_colors = "#d3d3d3#000000" + flags_1 = IS_PLAYER_COLORABLE_1 + /obj/item/toy/plush/lizard_plushie/space name = "space lizard plushie" desc = "An adorable stuffed toy that resembles a very determined spacefaring lizardperson. To infinity and beyond, little guy." diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index 91481bb9a3e..436e484cb43 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -209,7 +209,11 @@ ADMIN_VERB(create_mapping_job_icons, R_DEBUG, "Generate job landmarks icons", "G for(var/obj/item/I in D) qdel(I) randomize_human(D) - D.dress_up_as_job(JB, TRUE) + D.dress_up_as_job( + equipping = JB, + visual_only = TRUE, + consistent = TRUE, + ) var/icon/I = icon(getFlatIcon(D), frame = 1) final.Insert(I, JB.title) qdel(D) diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm index 0a9422e07b8..28984ce2272 100644 --- a/code/modules/antagonists/spy/spy_bounty.dm +++ b/code/modules/antagonists/spy/spy_bounty.dm @@ -219,6 +219,8 @@ continue if(!is_station_level(thing_turf.z) && !is_mining_level(thing_turf.z)) continue + if(HAS_TRAIT(existing_thing, TRAIT_ITEM_OBJECTIVE_BLOCKED)) + continue all_valid_existing_things += existing_thing if(!length(all_valid_existing_things)) @@ -253,16 +255,14 @@ return FALSE desired_item = pick(valid_possible_items) - // We need to do some snowflake for items that do exist vs generic items - var/list/obj/item/existing_items = GLOB.steal_item_handler.objectives_by_path[desired_item.targetitem] - var/obj/item/the_item = length(existing_items) ? pick(existing_items) : desired_item.targetitem - var/the_item_name = istype(the_item) ? the_item.name : initial(the_item.name) - name = "[the_item_name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft" - help = "Steal any [the_item_name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]" + name = "[desired_item.name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft" + help = "Steal [desired_item.name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]" return TRUE /datum/spy_bounty/objective_item/is_stealable(atom/movable/stealing) - return istype(stealing, desired_item.targetitem) && desired_item.check_special_completion(stealing) + return istype(stealing, desired_item.targetitem) \ + && !HAS_TRAIT(stealing, TRAIT_ITEM_OBJECTIVE_BLOCKED) \ + && desired_item.check_special_completion(stealing) /datum/spy_bounty/objective_item/random_easy difficulty = SPY_DIFFICULTY_EASY diff --git a/code/modules/antagonists/traitor/objectives/steal.dm b/code/modules/antagonists/traitor/objectives/steal.dm index 22d8ed7d39c..e9560498de3 100644 --- a/code/modules/antagonists/traitor/objectives/steal.dm +++ b/code/modules/antagonists/traitor/objectives/steal.dm @@ -29,6 +29,8 @@ GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new()) /datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item) SIGNAL_HANDLER + if(HAS_TRAIT(item, TRAIT_ITEM_OBJECTIVE_BLOCKED)) + return if(!generated_items) item.add_stealing_item_objective() return @@ -224,6 +226,8 @@ GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new()) /datum/traitor_objective/steal_item/proc/handle_special_case(obj/item/source, obj/item/target) SIGNAL_HANDLER + if(HAS_TRAIT(target, TRAIT_ITEM_OBJECTIVE_BLOCKED)) + return COMPONENT_FORCE_FAIL_PLACEMENT if(istype(target, target_item.targetitem)) if(!target_item.check_special_completion(target)) return COMPONENT_FORCE_FAIL_PLACEMENT diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 0e5d018be37..8eb0def90bf 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -225,8 +225,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) return TRUE if ("rotate") - character_preview_view.dir = turn(character_preview_view.dir, -90) - + character_preview_view.setDir(turn(character_preview_view.dir, -90)) return TRUE if ("set_preference") var/requested_preference_key = params["preference"] @@ -351,6 +350,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/mob/living/carbon/human/dummy/body /// The preferences this refers to var/datum/preferences/preferences + /// Whether we show current job clothes or nude/loadout only + var/show_job_clothes = TRUE /atom/movable/screen/map_view/char_preview/Initialize(mapload, datum/preferences/preferences) . = ..() @@ -368,16 +369,14 @@ GLOBAL_LIST_EMPTY(preferences_datums) create_body() else body.wipe_state() - appearance = preferences.render_new_preview_appearance(body) + + appearance = preferences.render_new_preview_appearance(body, show_job_clothes) /atom/movable/screen/map_view/char_preview/proc/create_body() QDEL_NULL(body) body = new - // Without this, it doesn't show up in the menu - body.appearance_flags &= ~KEEP_TOGETHER - /datum/preferences/proc/create_character_profiles() var/list/profiles = list() diff --git a/code/modules/client/preferences/_preference.dm b/code/modules/client/preferences/_preference.dm index 5fbf5c6953d..6c62ed1d054 100644 --- a/code/modules/client/preferences/_preference.dm +++ b/code/modules/client/preferences/_preference.dm @@ -18,11 +18,16 @@ /// support the "use gender" option. #define PREFERENCE_PRIORITY_BODY_TYPE 5 +/// Equpping items based on preferences. +/// Should happen after species and body type to make sure it looks right. +/// Mostly redundant, but a safety net for saving/loading. +#define PREFERENCE_PRIORITY_LOADOUT 6 + /// The priority at which names are decided, needed for proper randomization. -#define PREFERENCE_PRIORITY_NAMES 6 +#define PREFERENCE_PRIORITY_NAMES 7 /// Preferences that aren't names, but change the name changes set by PREFERENCE_PRIORITY_NAMES. -#define PREFERENCE_PRIORITY_NAME_MODIFICATIONS 7 +#define PREFERENCE_PRIORITY_NAME_MODIFICATIONS 8 /// The maximum preference priority, keep this updated, but don't use it for `priority`. #define MAX_PREFERENCE_PRIORITY PREFERENCE_PRIORITY_NAME_MODIFICATIONS diff --git a/code/modules/client/preferences/migrations/quirk_loadout_migration.dm b/code/modules/client/preferences/migrations/quirk_loadout_migration.dm new file mode 100644 index 00000000000..52fcd993c57 --- /dev/null +++ b/code/modules/client/preferences/migrations/quirk_loadout_migration.dm @@ -0,0 +1,27 @@ +/** + * Move quirk items into loadout items + * + * If this is accompanied with removal of a quirk, + * you don't need to worry about handling that here - + * quirk sanitization happens AFTER migration + */ +/datum/preferences/proc/migrate_quirk_to_loadout(quirk_to_migrate, new_typepath, list/data_to_migrate) + ASSERT(istext(quirk_to_migrate) && ispath(new_typepath, /obj/item)) + if(quirk_to_migrate in all_quirks) + add_loadout_item(new_typepath, data_to_migrate) + +/// Helper for slotting in a new loadout item +/datum/preferences/proc/add_loadout_item(typepath, list/data = list()) + PRIVATE_PROC(TRUE) + + var/list/loadout_list = read_preference(/datum/preference/loadout) || list() + loadout_list[typepath] = data + write_preference(GLOB.preference_entries[/datum/preference/loadout], loadout_list) + +/// Helper for removing a loadout item +/datum/preferences/proc/remove_loadout_item(typepath) + PRIVATE_PROC(TRUE) + + var/list/loadout_list = read_preference(/datum/preference/loadout) + if(loadout_list?.Remove(typepath)) + write_preference(GLOB.preference_entries[/datum/preference/loadout], loadout_list) diff --git a/code/modules/client/preferences/pride_pin.dm b/code/modules/client/preferences/pride_pin.dm deleted file mode 100644 index 326dad69979..00000000000 --- a/code/modules/client/preferences/pride_pin.dm +++ /dev/null @@ -1,16 +0,0 @@ -/datum/preference/choiced/pride_pin - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES - savefile_key = "pride_pin" - savefile_identifier = PREFERENCE_CHARACTER - -/datum/preference/choiced/pride_pin/init_possible_values() - return assoc_to_keys(GLOB.pride_pin_reskins) - -/datum/preference/choiced/pride_pin/is_accessible(datum/preferences/preferences) - if (!..(preferences)) - return FALSE - - return "Pride Pin" in preferences.all_quirks - -/datum/preference/choiced/pride_pin/apply_to_human(mob/living/carbon/human/target, value) - return diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 3d930ce8989..aa6b1840c50 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -5,7 +5,7 @@ // You do not need to raise this if you are adding new values that have sane defaults. // Only raise this value when changing the meaning/format/name/layout of an existing value // where you would want the updater procs below to run -#define SAVEFILE_VERSION_MAX 44 +#define SAVEFILE_VERSION_MAX 45 /* SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn @@ -104,6 +104,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car if (current_version < 43) migrate_legacy_sound_toggles(savefile) + if (current_version < 45) + migrate_quirk_to_loadout( + quirk_to_migrate = "Pride Pin", + new_typepath = /obj/item/clothing/accessory/pride, + data_to_migrate = list(INFO_RESKIN = save_data?["pride_pin"]), + ) + /// checks through keybindings for outdated unbound keys and updates them /datum/preferences/proc/check_keybindings() if(!parent) diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm index 6fbc2232b1b..3316eb8c52c 100644 --- a/code/modules/clothing/head/hat.dm +++ b/code/modules/clothing/head/hat.dm @@ -84,6 +84,8 @@ name = "flat cap" desc = "A working man's cap." icon_state = "beret_flat" + icon_preview = 'icons/obj/clothing/head/beret.dmi' + icon_state_preview = "beret_flat" greyscale_config = /datum/greyscale_config/beret greyscale_config_worn = /datum/greyscale_config/beret/worn greyscale_colors = "#8F7654" diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm index 1ea2acb78a6..cd1f6a2798b 100644 --- a/code/modules/clothing/head/jobs.dm +++ b/code/modules/clothing/head/jobs.dm @@ -310,7 +310,7 @@ else balloon_alert(wearer, "can't put in hands!") break - + return . /obj/item/clothing/head/fedora/inspector_hat/attackby(obj/item/item, mob/user, params) @@ -385,6 +385,8 @@ name = "beret" desc = "A beret, a mime's favorite headwear." icon_state = "beret" + icon_preview = 'icons/obj/clothing/head/beret.dmi' + icon_state_preview = "beret" dog_fashion = /datum/dog_fashion/head/beret greyscale_config = /datum/greyscale_config/beret greyscale_config_worn = /datum/greyscale_config/beret/worn diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm index 7a3c0f1150d..c49fbe3246d 100644 --- a/code/modules/clothing/neck/_neck.dm +++ b/code/modules/clothing/neck/_neck.dm @@ -398,6 +398,8 @@ /obj/item/clothing/neck/large_scarf name = "large scarf" icon_state = "large_scarf" + icon_preview = 'icons/obj/fluff/previews.dmi' + icon_state_preview = "scarf_large" w_class = WEIGHT_CLASS_TINY custom_price = PAYCHECK_CREW greyscale_colors = "#C6C6C6#EEEEEE" diff --git a/code/modules/clothing/under/accessories/badges.dm b/code/modules/clothing/under/accessories/badges.dm index bbafb4a132b..0ea3922893a 100644 --- a/code/modules/clothing/under/accessories/badges.dm +++ b/code/modules/clothing/under/accessories/badges.dm @@ -175,26 +175,23 @@ name = "Pre-Approved Cyborg Candidate dogtag" display = "This employee has been screened for negative mental traits to an acceptable level of accuracy, and is approved for the NT Cyborg program as an alternative to medical resuscitation." -/// Reskins for the pride pin accessory, mapped by display name to icon state -GLOBAL_LIST_INIT(pride_pin_reskins, list( - "Rainbow Pride" = "pride", - "Bisexual Pride" = "pride_bi", - "Pansexual Pride" = "pride_pan", - "Asexual Pride" = "pride_ace", - "Non-binary Pride" = "pride_enby", - "Transgender Pride" = "pride_trans", - "Intersex Pride" = "pride_intersex", - "Lesbian Pride" = "pride_lesbian", -)) - /obj/item/clothing/accessory/pride name = "pride pin" desc = "A Nanotrasen Diversity & Inclusion Center-sponsored holographic pin to show off your pride, reminding the crew of their unwavering commitment to equity, diversity, and inclusion!" icon_state = "pride" obj_flags = UNIQUE_RENAME | INFINITE_RESKIN + unique_reskin = list( + "Rainbow Pride" = "pride", + "Bisexual Pride" = "pride_bi", + "Pansexual Pride" = "pride_pan", + "Asexual Pride" = "pride_ace", + "Non-binary Pride" = "pride_enby", + "Transgender Pride" = "pride_trans", + "Intersex Pride" = "pride_intersex", + "Lesbian Pride" = "pride_lesbian", + ) /obj/item/clothing/accessory/pride/setup_reskinning() - unique_reskin = GLOB.pride_pin_reskins if(!check_setup_reskinning()) return diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index da9fafbd0c9..dd0a8dcd47c 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -173,7 +173,7 @@ spawned_human.mind.adjust_experience(i, roundstart_experience[i], TRUE) /// Return the outfit to use -/datum/job/proc/get_outfit() +/datum/job/proc/get_outfit(consistent) return outfit /// Announce that this job as joined the round to all crew members. @@ -188,12 +188,12 @@ return TRUE -/mob/living/proc/on_job_equipping(datum/job/equipping) +/mob/living/proc/on_job_equipping(datum/job/equipping, client/player_client) return #define VERY_LATE_ARRIVAL_TOAST_PROB 20 -/mob/living/carbon/human/on_job_equipping(datum/job/equipping) +/mob/living/carbon/human/on_job_equipping(datum/job/equipping, client/player_client) if(equipping.paycheck_department) var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier) bank_account.payday(STARTING_PAYCHECKS, TRUE) @@ -201,19 +201,24 @@ bank_account.replaceable = FALSE add_mob_memory(/datum/memory/key/account, remembered_id = account_id) - dress_up_as_job(equipping) + dress_up_as_job( + equipping = equipping, + visual_only = FALSE, + player_client = player_client, + consistent = FALSE, + ) if(EMERGENCY_PAST_POINT_OF_NO_RETURN && prob(VERY_LATE_ARRIVAL_TOAST_PROB)) equip_to_slot_or_del(new /obj/item/food/griddle_toast(src), ITEM_SLOT_MASK) #undef VERY_LATE_ARRIVAL_TOAST_PROB -/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE) +/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE, client/player_client, consistent = FALSE) return -/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE) +/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE, client/player_client, consistent = FALSE) dna.species.pre_equip_species_outfit(equipping, src, visual_only) - equipOutfit(equipping.get_outfit(), visual_only) + equip_outfit_and_loadout(equipping.get_outfit(consistent), player_client?.prefs, visual_only) /datum/job/proc/announce_head(mob/living/carbon/human/H, channels) //tells the given channel that the given mob is the new department head. See communications.dm for valid channels. if(H && GLOB.announcement_systems.len) diff --git a/code/modules/jobs/job_types/assistant/assistant.dm b/code/modules/jobs/job_types/assistant/assistant.dm index c97c987cb58..2732d5e21ca 100644 --- a/code/modules/jobs/job_types/assistant/assistant.dm +++ b/code/modules/jobs/job_types/assistant/assistant.dm @@ -37,7 +37,9 @@ Assistant rpg_title = "Lout" config_tag = "ASSISTANT" -/datum/job/assistant/get_outfit() +/datum/job/assistant/get_outfit(consistent) + if(consistent) + return /datum/outfit/job/assistant/consistent if(!HAS_TRAIT(SSstation, STATION_TRAIT_ASSISTANT_GIMMICKS)) return ..() diff --git a/code/modules/loadout/categories/accessories.dm b/code/modules/loadout/categories/accessories.dm new file mode 100644 index 00000000000..0dd655358bd --- /dev/null +++ b/code/modules/loadout/categories/accessories.dm @@ -0,0 +1,92 @@ +/// Accessory Items (Moves overrided items to backpack) +/datum/loadout_category/accessories + category_name = "Accessory" + category_ui_icon = FA_ICON_VEST + type_to_generate = /datum/loadout_item/accessory + tab_order = /datum/loadout_category/head::tab_order + 3 + +/datum/loadout_item/accessory + abstract_type = /datum/loadout_item/accessory + /// Can we adjust this accessory to be above or below suits? + VAR_FINAL/can_be_layer_adjusted = FALSE + +/datum/loadout_item/accessory/New() + . = ..() + if(ispath(item_path, /obj/item/clothing/accessory)) + can_be_layer_adjusted = TRUE + +/datum/loadout_item/accessory/get_ui_buttons() + if(!can_be_layer_adjusted) + return ..() + + var/list/buttons = ..() + + UNTYPED_LIST_ADD(buttons, list( + "label" = "Layer", + "act_key" = "set_layer", + "active_key" = INFO_LAYER, + "active_text" = "Above Suit", + "inactive_text" = "Below Suit", + )) + + return buttons + +/datum/loadout_item/accessory/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params) + if(action == "set_layer") + return set_accessory_layer(manager, user) + + return ..() + +/datum/loadout_item/accessory/proc/set_accessory_layer(datum/preference_middleware/loadout/manager, mob/user) + if(!can_be_layer_adjusted) + return FALSE + + var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) + if(!loadout?[item_path]) + return FALSE + + if(isnull(loadout[item_path][INFO_LAYER])) + loadout[item_path][INFO_LAYER] = FALSE + + loadout[item_path][INFO_LAYER] = !loadout[item_path][INFO_LAYER] + manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + return TRUE // Update UI + +/datum/loadout_item/accessory/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + if(outfit.accessory) + LAZYADD(outfit.backpack_contents, outfit.accessory) + outfit.accessory = item_path + +/datum/loadout_item/accessory/on_equip_item( + obj/item/clothing/accessory/equipped_item, + datum/preferences/preference_source, + list/preference_list, + mob/living/carbon/human/equipper, + visuals_only = FALSE, +) + . = ..() + if(istype(equipped_item)) + equipped_item.above_suit = !!preference_list[item_path]?[INFO_LAYER] + . |= (ITEM_SLOT_OCLOTHING|ITEM_SLOT_ICLOTHING) + +/datum/loadout_item/accessory/maid_apron + name = "Maid Apron" + item_path = /obj/item/clothing/accessory/maidapron + +/datum/loadout_item/accessory/waistcoat + name = "Waistcoat" + item_path = /obj/item/clothing/accessory/waistcoat + +/datum/loadout_item/accessory/pocket_protector + name = "Pocket Protector" + item_path = /obj/item/clothing/accessory/pocketprotector + +/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" + item_path = /obj/item/clothing/accessory/pride + can_be_reskinned = TRUE diff --git a/code/modules/loadout/categories/glasses.dm b/code/modules/loadout/categories/glasses.dm new file mode 100644 index 00000000000..5b8ff856200 --- /dev/null +++ b/code/modules/loadout/categories/glasses.dm @@ -0,0 +1,55 @@ +/// Glasses Slot Items (Moves overrided items to backpack) +/datum/loadout_category/glasses + category_name = "Glasses" + category_ui_icon = FA_ICON_GLASSES + type_to_generate = /datum/loadout_item/glasses + tab_order = /datum/loadout_category/head::tab_order + 1 + +/datum/loadout_item/glasses + abstract_type = /datum/loadout_item/glasses + +/datum/loadout_item/glasses/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + if(outfit.glasses) + LAZYADD(outfit.backpack_contents, outfit.glasses) + outfit.glasses = item_path + +/datum/loadout_item/glasses/prescription_glasses + name = "Glasses" + item_path = /obj/item/clothing/glasses/regular + additional_displayed_text = list("Prescription") + +/datum/loadout_item/glasses/prescription_glasses/circle_glasses + name = "Circle Glasses" + item_path = /obj/item/clothing/glasses/regular/circle + +/datum/loadout_item/glasses/prescription_glasses/hipster_glasses + name = "Hipster Glasses" + item_path = /obj/item/clothing/glasses/regular/hipster + +/datum/loadout_item/glasses/prescription_glasses/jamjar_glasses + name = "Jamjar Glasses" + item_path = /obj/item/clothing/glasses/regular/jamjar + +/datum/loadout_item/glasses/black_blindfold + name = "Black Blindfold" + item_path = /obj/item/clothing/glasses/blindfold + +/datum/loadout_item/glasses/cold_glasses + name = "Cold Glasses" + item_path = /obj/item/clothing/glasses/cold + +/datum/loadout_item/glasses/heat_glasses + name = "Heat Glasses" + item_path = /obj/item/clothing/glasses/heat + +/datum/loadout_item/glasses/orange_glasses + name = "Orange Glasses" + item_path = /obj/item/clothing/glasses/orange + +/datum/loadout_item/glasses/red_glasses + name = "Red Glasses" + item_path = /obj/item/clothing/glasses/red + +/datum/loadout_item/glasses/eyepatch + name = "Eyepatch" + item_path = /obj/item/clothing/glasses/eyepatch diff --git a/code/modules/loadout/categories/heads.dm b/code/modules/loadout/categories/heads.dm new file mode 100644 index 00000000000..6b939495684 --- /dev/null +++ b/code/modules/loadout/categories/heads.dm @@ -0,0 +1,138 @@ +/// Head Slot Items (Deletes overrided items) +/datum/loadout_category/head + category_name = "Head" + category_ui_icon = FA_ICON_HAT_COWBOY + type_to_generate = /datum/loadout_item/head + tab_order = 1 + +/datum/loadout_item/head + abstract_type = /datum/loadout_item/head + +/datum/loadout_item/head/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + if(equipper.dna?.species?.outfit_important_for_life) + if(!visuals_only) + to_chat(equipper, "Your loadout helmet was not equipped directly due to your species outfit.") + LAZYADD(outfit.backpack_contents, item_path) + else + outfit.head = item_path + +/datum/loadout_item/head/beanie + name = "Beanie (Colorable)" + item_path = /obj/item/clothing/head/beanie + +/datum/loadout_item/head/fancy_cap + name = "Fancy Hat (Colorable)" + item_path = /obj/item/clothing/head/costume/fancy + +/datum/loadout_item/head/red_beret + name = "Red Beret (Colorable)" + item_path = /obj/item/clothing/head/beret + +/datum/loadout_item/head/black_cap + name = "Cap (Black)" + item_path = /obj/item/clothing/head/soft/black + +/datum/loadout_item/head/blue_cap + name = "Cap (Blue)" + item_path = /obj/item/clothing/head/soft/blue + +/datum/loadout_item/head/delinquent_cap + name = "Cap (Delinquent)" + item_path = /obj/item/clothing/head/costume/delinquent + +/datum/loadout_item/head/green_cap + name = "Cap (Green)" + item_path = /obj/item/clothing/head/soft/green + +/datum/loadout_item/head/grey_cap + name = "Cap (Grey)" + item_path = /obj/item/clothing/head/soft/grey + +/datum/loadout_item/head/orange_cap + name = "Cap (Orange)" + item_path = /obj/item/clothing/head/soft/orange + +/datum/loadout_item/head/purple_cap + name = "Cap (Purple)" + item_path = /obj/item/clothing/head/soft/purple + +/datum/loadout_item/head/rainbow_cap + name = "Cap (Rainbow)" + item_path = /obj/item/clothing/head/soft/rainbow + +/datum/loadout_item/head/red_cap + name = "Cap (Red)" + item_path = /obj/item/clothing/head/soft/red + +/datum/loadout_item/head/white_cap + name = "Cap (White)" + item_path = /obj/item/clothing/head/soft + +/datum/loadout_item/head/yellow_cap + name = "Cap (Yellow)" + item_path = /obj/item/clothing/head/soft/yellow + +/datum/loadout_item/head/flatcap + name = "Cap (Flat)" + item_path = /obj/item/clothing/head/flatcap + +/datum/loadout_item/head/beige_fedora + name = "Fedora (Beige)" + item_path = /obj/item/clothing/head/fedora/beige + +/datum/loadout_item/head/black_fedora + name = "Fedora (Black)" + item_path = /obj/item/clothing/head/fedora + +/datum/loadout_item/head/white_fedora + name = "Fedora (White)" + item_path = /obj/item/clothing/head/fedora/white + +/datum/loadout_item/head/mail_cap + name = "Cap (Mail)" + item_path = /obj/item/clothing/head/costume/mailman + +/datum/loadout_item/head/kitty_ears + name = "Kitty Ears" + item_path = /obj/item/clothing/head/costume/kitty + +/datum/loadout_item/head/rabbit_ears + name = "Rabbit Ears" + item_path = /obj/item/clothing/head/costume/rabbitears + +/datum/loadout_item/head/bandana + name = "Bandana Thin" + item_path = /obj/item/clothing/head/costume/tmc + +/datum/loadout_item/head/rastafarian + name = "Cap (Rastafarian)" + item_path = /obj/item/clothing/head/rasta + +/datum/loadout_item/head/top_hat + name = "Top Hat" + item_path = /obj/item/clothing/head/hats/tophat + +/datum/loadout_item/head/bowler_hat + name = "Bowler Hat" + item_path = /obj/item/clothing/head/hats/bowler + +/datum/loadout_item/head/bear_pelt + name = "Bear Pelt" + item_path = /obj/item/clothing/head/costume/bearpelt + +/datum/loadout_item/head/ushanka + name ="Ushanka" + item_path = /obj/item/clothing/head/costume/ushanka + +/datum/loadout_item/head/plague_doctor + name = "Cap (Plague Doctor)" + item_path = /obj/item/clothing/head/bio_hood/plague + +/datum/loadout_item/head/rose + name = "Rose" + item_path = /obj/item/food/grown/rose + +/datum/loadout_item/head/wig + name = "Wig" + item_path = /obj/item/clothing/head/wig/natural + additional_displayed_text = list("Hair Color") diff --git a/code/modules/loadout/categories/inhands.dm b/code/modules/loadout/categories/inhands.dm new file mode 100644 index 00000000000..8ddc15676ab --- /dev/null +++ b/code/modules/loadout/categories/inhands.dm @@ -0,0 +1,33 @@ +/// Inhand items (Moves overrided items to backpack) +/datum/loadout_category/inhands + category_name = "Inhand" + category_ui_icon = FA_ICON_BRIEFCASE + type_to_generate = /datum/loadout_item/inhand + tab_order = /datum/loadout_category/head::tab_order + 4 + +/datum/loadout_item/inhand + abstract_type = /datum/loadout_item/inhand + +/datum/loadout_item/inhand/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + if(outfit.l_hand && !outfit.r_hand) + outfit.r_hand = item_path + else + if(outfit.l_hand) + LAZYADD(outfit.backpack_contents, outfit.l_hand) + outfit.l_hand = item_path + +/datum/loadout_item/inhand/cane + name = "Cane" + item_path = /obj/item/cane + +/datum/loadout_item/inhand/cane_white + name = "White Cane" + item_path = /obj/item/cane/white + +/datum/loadout_item/inhand/briefcase + name = "Briefcase (Leather)" + item_path = /obj/item/storage/briefcase + +/datum/loadout_item/inhand/briefcase_secure + name = "Briefcase (Secure)" + item_path = /obj/item/storage/briefcase/secure diff --git a/code/modules/loadout/categories/neck.dm b/code/modules/loadout/categories/neck.dm new file mode 100644 index 00000000000..a48597b3b9e --- /dev/null +++ b/code/modules/loadout/categories/neck.dm @@ -0,0 +1,36 @@ +/// Neck Slot Items (Deletes overrided items) +/datum/loadout_category/neck + category_name = "Neck" + category_ui_icon = FA_ICON_USER_TIE + type_to_generate = /datum/loadout_item/neck + tab_order = /datum/loadout_category/head::tab_order + 2 + +/datum/loadout_item/neck + abstract_type = /datum/loadout_item/neck + +/datum/loadout_item/neck/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + outfit.neck = item_path + +/datum/loadout_item/neck/scarf_greyscale + name = "Scarf (Colorable)" + item_path = /obj/item/clothing/neck/scarf + +/datum/loadout_item/neck/greyscale_large + name = "Scarf (Large, Colorable)" + item_path = /obj/item/clothing/neck/large_scarf + +/datum/loadout_item/neck/greyscale_larger + name = "Scarf (Larger, Colorable)" + item_path = /obj/item/clothing/neck/infinity_scarf + +/datum/loadout_item/neck/necktie + name = "Necktie (Colorable)" + item_path = /obj/item/clothing/neck/tie + +/datum/loadout_item/neck/necktie_disco + name = "Necktie (Ugly)" + item_path = /obj/item/clothing/neck/tie/horrible + +/datum/loadout_item/neck/necktie_loose + name = "Necktie (Loose)" + item_path = /obj/item/clothing/neck/tie/detective diff --git a/code/modules/loadout/categories/pocket.dm b/code/modules/loadout/categories/pocket.dm new file mode 100644 index 00000000000..e1cddde76e5 --- /dev/null +++ b/code/modules/loadout/categories/pocket.dm @@ -0,0 +1,203 @@ +/// Pocket items (Moved to backpack) +/datum/loadout_category/pocket + category_name = "Other" + category_ui_icon = FA_ICON_QUESTION + type_to_generate = /datum/loadout_item/pocket_items + tab_order = /datum/loadout_category/head::tab_order + 5 + /// How many pocket items are allowed + VAR_PRIVATE/max_allowed = 2 + +/datum/loadout_category/pocket/New() + . = ..() + category_info = "([max_allowed] allowed)" + +/datum/loadout_category/pocket/handle_duplicate_entires( + datum/preference_middleware/loadout/manager, + datum/loadout_item/conflicting_item, + datum/loadout_item/added_item, + list/datum/loadout_item/all_loadout_items, +) + var/list/datum/loadout_item/pocket_items/other_pocket_items = list() + for(var/datum/loadout_item/pocket_items/other_pocket_item in all_loadout_items) + other_pocket_items += other_pocket_item + + if(length(other_pocket_items) >= max_allowed) + // We only need to deselect something if we're above the limit + // (And if we are we prioritize the first item found, FIFO) + manager.deselect_item(other_pocket_items[1]) + return TRUE + +/datum/loadout_item/pocket_items + abstract_type = /datum/loadout_item/pocket_items + +/datum/loadout_item/pocket_items/on_equip_item( + obj/item/equipped_item, + datum/preferences/preference_source, + list/preference_list, + mob/living/carbon/human/equipper, + visuals_only = FALSE, +) + // Backpack items aren't created if it's a visual equipping, so don't do any on equip stuff. It doesn't exist. + if(visuals_only) + return NONE + + return ..() + +/datum/loadout_item/pocket_items/lipstick_black + name = "Lipstick (Black)" + item_path = /obj/item/lipstick/black + additional_displayed_text = list("Black") + +/datum/loadout_item/pocket_items/lipstick_blue + name = "Lipstick (Blue)" + item_path = /obj/item/lipstick/blue + additional_displayed_text = list("Blue") + + +/datum/loadout_item/pocket_items/lipstick_green + name = "Lipstick (Green)" + item_path = /obj/item/lipstick/green + additional_displayed_text = list("Green") + + +/datum/loadout_item/pocket_items/lipstick_jade + name = "Lipstick (Jade)" + item_path = /obj/item/lipstick/jade + additional_displayed_text = list("Jade") + +/datum/loadout_item/pocket_items/lipstick_purple + name = "Lipstick (Purple)" + item_path = /obj/item/lipstick/purple + additional_displayed_text = list("Purple") + +/datum/loadout_item/pocket_items/lipstick_red + name = "Lipstick (Red)" + item_path = /obj/item/lipstick + additional_displayed_text = list("Red") + +/datum/loadout_item/pocket_items/lipstick_white + name = "Lipstick (White)" + item_path = /obj/item/lipstick/white + additional_displayed_text = list("White") + +/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/narsie + name = "Plush (Nar'sie)" + item_path = /obj/item/toy/plush/narplush + +/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/ratvar + name = "Plush (Ratvar)" + item_path = /obj/item/toy/plush/ratplush + +/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 diff --git a/code/modules/loadout/loadout_categories.dm b/code/modules/loadout/loadout_categories.dm new file mode 100644 index 00000000000..760d3f59798 --- /dev/null +++ b/code/modules/loadout/loadout_categories.dm @@ -0,0 +1,79 @@ +/** + * # Loadout categories + * + * Loadout categories are singletons used to group loadout items together in the loadout screen. + */ +/datum/loadout_category + /// The name of the category, shown in the tabs + var/category_name + /// FontAwesome icon for the category + var/category_ui_icon + /// String to display on the top-right of a category tab + var/category_info + /// Order which they appear in the tabs, ties go alphabetically + var/tab_order = -1 + /// What type of loadout items should be generated for this category? + var/type_to_generate + /// List of all loadout items in this category + VAR_FINAL/list/datum/loadout_item/associated_items + +/datum/loadout_category/New() + . = ..() + associated_items = get_items() + for(var/datum/loadout_item/item as anything in associated_items) + if(GLOB.all_loadout_datums[item.item_path]) + stack_trace("Loadout datum collision - [item.item_path] is shared between multiple loadout datums.") + GLOB.all_loadout_datums[item.item_path] = item + +/datum/loadout_category/Destroy(force, ...) + if(!force) + stack_trace("QDEL called on loadout category [type]. This shouldn't ever happen. (Use FORCE if necessary.)") + return QDEL_HINT_LETMELIVE + + associated_items.Cut() + return ..() + +/// Return a list of all /datum/loadout_items in this category. +/datum/loadout_category/proc/get_items() as /list + var/list/all_items = list() + for(var/datum/loadout_item/found_type as anything in typesof(type_to_generate)) + if(found_type == initial(found_type.abstract_type)) + continue + + if(!ispath(initial(found_type.item_path), /obj/item)) + stack_trace("Loadout get_items(): Attempted to instantiate a loadout item ([found_type]) with an invalid or null typepath! (got path: [initial(found_type.item_path)])") + continue + + var/datum/loadout_item/spawned_type = new found_type(src) + all_items += spawned_type + + return all_items + +/// Returns a list of all /datum/loadout_items in this category, formatted for UI use. Only ran once. +/datum/loadout_category/proc/items_to_ui_data() as /list + if(!length(associated_items)) + return list() + + var/list/formatted_list = list() + + for(var/datum/loadout_item/item as anything in associated_items) + var/list/item_data = item.to_ui_data() + UNTYPED_LIST_ADD(formatted_list, item_data) + + sortTim(formatted_list, /proc/cmp_assoc_list_name) // Alphabetizing + return formatted_list + +/** + * Handles what happens when two items of this category are selected at once + * + * Return TRUE if it's okay to continue with adding the incoming item, + * or return FALSE to stop the new item from being added + */ +/datum/loadout_category/proc/handle_duplicate_entires( + datum/preference_middleware/loadout/manager, + datum/loadout_item/conflicting_item, + datum/loadout_item/added_item, + list/datum/loadout_item/all_loadout_items, +) + manager.deselect_item(conflicting_item) + return TRUE diff --git a/code/modules/loadout/loadout_helpers.dm b/code/modules/loadout/loadout_helpers.dm new file mode 100644 index 00000000000..bbab79b6333 --- /dev/null +++ b/code/modules/loadout/loadout_helpers.dm @@ -0,0 +1,80 @@ +/** + * Equips this mob with a given outfit and loadout items as per the passed preferences. + * + * Loadout items override the pre-existing item in the corresponding slot of the job outfit. + * Some job items are preserved after being overridden - belt items, ear items, and glasses. + * The rest of the slots, the items are overridden completely and deleted. + * + * Species with special outfits are snowflaked to have loadout items placed in their bags instead of overriding the outfit. + * + * * outfit - the job outfit we're equipping + * * preference_source - the preferences to draw loadout items from. + * * visuals_only - whether we call special equipped procs, or if we just look like we equipped it + */ +/mob/living/carbon/human/proc/equip_outfit_and_loadout( + datum/outfit/outfit = /datum/outfit, + datum/preferences/preference_source, + visuals_only = FALSE, +) + if(isnull(preference_source)) + return equipOutfit(outfit, visuals_only) + + var/datum/outfit/equipped_outfit + if(ispath(outfit, /datum/outfit)) + equipped_outfit = new outfit() + else if(istype(outfit, /datum/outfit)) + equipped_outfit = outfit + else + CRASH("Invalid outfit passed to equip_outfit_and_loadout ([outfit])") + + var/list/preference_list = preference_source.read_preference(/datum/preference/loadout) + var/list/loadout_datums = loadout_list_to_datums(preference_list) + // Slap our things into the outfit given + for(var/datum/loadout_item/item as anything in loadout_datums) + item.insert_path_into_outfit(equipped_outfit, src, visuals_only) + // Equip the outfit loadout items included + if(!equipped_outfit.equip(src, visuals_only)) + return FALSE + // Handle any snowflake on_equips + var/list/new_contents = get_all_gear() + var/update = NONE + for(var/datum/loadout_item/item as anything in loadout_datums) + var/obj/item/equipped = locate(item.item_path) in new_contents + if(isnull(equipped)) + continue + update |= item.on_equip_item( + equipped_item = equipped, + preference_source = preference_source, + preference_list = preference_list, + equipper = src, + visuals_only = visuals_only, + ) + + if(update) + update_clothing(update) + + return TRUE + +/** + * Takes a list of paths (such as a loadout list) + * and returns a list of their singleton loadout item datums + * + * loadout_list - the list being checked + * + * Returns a list of singleton datums + */ +/proc/loadout_list_to_datums(list/loadout_list) as /list + var/list/datums = list() + + if(!length(GLOB.all_loadout_datums)) + CRASH("No loadout datums in the global loadout list!") + + for(var/path in loadout_list) + var/actual_datum = GLOB.all_loadout_datums[path] + if(!istype(actual_datum, /datum/loadout_item)) + stack_trace("Could not find ([path]) loadout item in the global list of loadout datums!") + continue + + datums += actual_datum + + return datums diff --git a/code/modules/loadout/loadout_items.dm b/code/modules/loadout/loadout_items.dm new file mode 100644 index 00000000000..53d0a7cc6cd --- /dev/null +++ b/code/modules/loadout/loadout_items.dm @@ -0,0 +1,371 @@ +/// Global list of ALL loadout datums instantiated. +/// Loadout datums are created by loadout categories. +GLOBAL_LIST_EMPTY(all_loadout_datums) + +/// Global list of all loadout categories +/// Doesn't really NEED to be a global but we need to init this early for preferences, +/// as the categories instantiate all the loadout datums +GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) + +/// Inits the global list of loadout category singletons +/// Also inits loadout item singletons +/proc/init_loadout_categories() + var/list/loadout_categories = list() + for(var/category_type in subtypesof(/datum/loadout_category)) + loadout_categories += new category_type() + + sortTim(loadout_categories, /proc/cmp_loadout_categories) + return loadout_categories + +/proc/cmp_loadout_categories(datum/loadout_category/A, datum/loadout_category/B) + var/a_order = A::tab_order + var/b_order = B::tab_order + if(a_order == b_order) + return cmp_text_asc(A::category_name, B::category_name) + return cmp_numeric_asc(a_order, b_order) + +/** + * # Loadout item datum + * + * Singleton that holds all the information about each loadout items, and how to equip them. + */ +/datum/loadout_item + /// The category of the loadout item. Set automatically in New + VAR_FINAL/datum/loadout_category/category + /// Displayed name of the loadout item. + /// Defaults to the item's name if unset. + var/name + /// 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]. + /// If you really want it to not be colorable set this to [DONT_GREYSCALE] + var/can_be_greyscale = FALSE + /// Whether this item can be renamed. + /// I recommend you apply this sparingly becuase it certainly can go wrong (or get reset / overridden easily) + var/can_be_named = FALSE + /// Whether this item can be reskinned. + /// Only works if the item has a "unique reskin" list set. + var/can_be_reskinned = FALSE + /// The abstract parent of this loadout item, to determine which items to not instantiate + 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 + /// Icon state for the UI to use for preview icons. + /// Set automatically if null + var/ui_icon_state + /// Reskin options of this item if it can be reskinned. + VAR_FINAL/list/cached_reskin_options + +/datum/loadout_item/New(category) + src.category = category + + if(can_be_greyscale == DONT_GREYSCALE) + can_be_greyscale = FALSE + else if(item_path::flags_1 & IS_PLAYER_COLORABLE_1) + can_be_greyscale = TRUE + + if(isnull(name)) + name = item_path::name + + if(isnull(ui_icon) && isnull(ui_icon_state)) + ui_icon = item_path::icon_preview || item_path::icon + ui_icon_state = item_path::icon_state_preview || item_path::icon_state + + if(can_be_reskinned) + var/obj/item/dummy_item = new item_path() + if(!length(dummy_item.unique_reskin)) + can_be_reskinned = FALSE + stack_trace("Loadout item [item_path] has can_be_reskinned set to TRUE but has no unique reskins.") + else + cached_reskin_options = dummy_item.unique_reskin.Copy() + qdel(dummy_item) + +/datum/loadout_item/Destroy(force, ...) + if(force) + stack_trace("QDEL called on loadout item [type]. This shouldn't ever happen. (Use FORCE if necessary.)") + return QDEL_HINT_LETMELIVE + + GLOB.all_loadout_datums -= item_path + return ..() + +/** + * Takes in an action from a loadout manager and applies it + * + * Useful for subtypes of loadout items with unique actions + * + * Return TRUE to force an update to the UI / character preview + */ +/datum/loadout_item/proc/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params) + SHOULD_CALL_PARENT(TRUE) + + switch(action) + if("select_color") + if(can_be_greyscale) + return set_item_color(manager, user) + + if("set_name") + if(can_be_named) + return set_name(manager, user) + + if("set_skin") + return set_skin(manager, user, params) + + return TRUE + +/// Opens up the GAGS editing menu. +/datum/loadout_item/proc/set_item_color(datum/preference_middleware/loadout/manager, mob/user) + if(manager.menu) + return FALSE + + var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) + var/list/allowed_configs = list() + if(initial(item_path.greyscale_config)) + allowed_configs += "[initial(item_path.greyscale_config)]" + if(initial(item_path.greyscale_config_worn)) + allowed_configs += "[initial(item_path.greyscale_config_worn)]" + if(initial(item_path.greyscale_config_inhand_left)) + allowed_configs += "[initial(item_path.greyscale_config_inhand_left)]" + if(initial(item_path.greyscale_config_inhand_right)) + allowed_configs += "[initial(item_path.greyscale_config_inhand_right)]" + + var/datum/greyscale_modify_menu/menu = new( + manager, + user, + allowed_configs, + CALLBACK(src, PROC_REF(set_slot_greyscale), manager), + starting_icon_state = initial(item_path.icon_state), + starting_config = initial(item_path.greyscale_config), + starting_colors = loadout?[item_path]?[INFO_GREYSCALE] || initial(item_path.greyscale_colors), + ) + + manager.register_greyscale_menu(menu) + menu.ui_interact(user) + return TRUE + +/// Callback for GAGS menu to set this item's color. +/datum/loadout_item/proc/set_slot_greyscale(datum/preference_middleware/loadout/manager, datum/greyscale_modify_menu/open_menu) + if(!istype(open_menu)) + CRASH("set_slot_greyscale called without a greyscale menu!") + + var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) + if(!loadout?[item_path]) + return FALSE + + var/list/colors = open_menu.split_colors + if(!colors) + return FALSE + + loadout[item_path][INFO_GREYSCALE] = colors.Join("") + manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + return TRUE // update UI + +/// Sets the name of the item. +/datum/loadout_item/proc/set_name(datum/preference_middleware/loadout/manager, mob/user) + var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) + var/input_name = tgui_input_text( + user = user, + message = "What name do you want to give the [name]? Leave blank to clear.", + title = "[name] name", + default = loadout?[item_path]?[INFO_NAMED], // plop in existing name (if any) + max_length = MAX_NAME_LEN, + ) + if(QDELETED(src) || QDELETED(user) || QDELETED(manager) || QDELETED(manager.preferences)) + return FALSE + + loadout = manager.preferences.read_preference(/datum/preference/loadout) // Make sure no shenanigans happened + if(!loadout?[item_path]) + return FALSE + + if(input_name) + loadout[item_path][INFO_NAMED] = input_name + else if(input_name == "") + loadout[item_path] -= INFO_NAMED + + manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + return FALSE // no update needed + +/// Used for reskinning an item to an alt skin. +/datum/loadout_item/proc/set_skin(datum/preference_middleware/loadout/manager, mob/user, params) + if(!can_be_reskinned) + return FALSE + + var/reskin_to = params["skin"] + if(!cached_reskin_options[reskin_to]) + return FALSE + + var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout) + if(!loadout?[item_path]) + return FALSE + + loadout[item_path][INFO_RESKIN] = reskin_to + manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + return TRUE // always update UI + +/** + * Place our [item_path] into the passed [outfit]. + * + * By default, just adds the item into the outfit's backpack contents, if non-visual. + * + * Arguments: + * * outfit - The outfit we're equipping our items into. + * * equipper - If we're equipping out outfit onto a mob at the time, this is the mob it is equipped on. Can be null. + * * visual - If TRUE, then our outfit is only for visual use (for example, a preview). + */ +/datum/loadout_item/proc/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + if(!visuals_only) + LAZYADD(outfit.backpack_contents, item_path) + +/** + * Called When the item is equipped on [equipper]. + * + * At this point the item is in the mob's contents + * + * Arguments: + * * preference_source - the datum/preferences our loadout item originated from - cannot be null + * * equipper - the mob we're equipping this item onto - cannot be null + * * visuals_only - whether or not this is only concerned with visual things (not backpack, not renaming, etc) + * * preference_list - what the raw loadout list looks like in the preferences + * + * Return a bitflag of slot flags to update + */ +/datum/loadout_item/proc/on_equip_item( + obj/item/equipped_item, + datum/preferences/preference_source, + list/preference_list, + mob/living/carbon/human/equipper, + visuals_only = FALSE, +) + ASSERT(!isnull(equipped_item)) + + if(!visuals_only) + ADD_TRAIT(equipped_item, TRAIT_ITEM_OBJECTIVE_BLOCKED, "Loadout") + + var/list/item_details = preference_list[item_path] + var/update_flag = NONE + + if(can_be_greyscale && item_details?[INFO_GREYSCALE]) + equipped_item.set_greyscale(item_details[INFO_GREYSCALE]) + update_flag |= equipped_item.slot_flags + + if(can_be_named && item_details?[INFO_NAMED] && !visuals_only) + equipped_item.name = trim(item_details[INFO_NAMED], PREVENT_CHARACTER_TRIM_LOSS(MAX_NAME_LEN)) + ADD_TRAIT(equipped_item, TRAIT_WAS_RENAMED, "Loadout") + + if(can_be_reskinned && item_details?[INFO_RESKIN]) + var/skin_chosen = item_details[INFO_RESKIN] + if(skin_chosen in equipped_item.unique_reskin) + equipped_item.current_skin = skin_chosen + equipped_item.icon_state = equipped_item.unique_reskin[skin_chosen] + if(istype(equipped_item, /obj/item/clothing/accessory)) + // Snowflake handing for accessories, because we need to update the thing it's attached to instead + if(isclothing(equipped_item.loc)) + var/obj/item/clothing/under/attached_to = equipped_item.loc + attached_to.update_accessory_overlay() + update_flag |= (ITEM_SLOT_OCLOTHING|ITEM_SLOT_ICLOTHING) + else + update_flag |= equipped_item.slot_flags + + else + // Not valid, update the preference + item_details -= INFO_RESKIN + preference_source.write_preference(GLOB.preference_entries[/datum/preference/loadout], preference_list) + + return update_flag + +/** + * Returns a formatted list of data for this loadout item. + */ +/datum/loadout_item/proc/to_ui_data() as /list + SHOULD_CALL_PARENT(TRUE) + + var/list/formatted_item = list() + formatted_item["name"] = name + formatted_item["path"] = item_path + formatted_item["information"] = get_item_information() + formatted_item["buttons"] = get_ui_buttons() + formatted_item["reskins"] = get_reskin_options() + formatted_item["icon"] = ui_icon + formatted_item["icon_state"] = ui_icon_state + return formatted_item + +/** + * Returns a list of information to display about this item in the loadout UI. + * + * These should be short strings, sub 14 characters generally. + */ +/datum/loadout_item/proc/get_item_information() as /list + SHOULD_CALL_PARENT(TRUE) + + 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" + + if(can_be_reskinned) + displayed_text += "Reskinnable" + + return displayed_text + +/** + * Returns a list of buttons that are shown in the loadout UI for customizing this item. + * + * Buttons contain + * - 'L'abel: The text displayed beside the button + * - act_key: The key that is sent to the loadout manager when the button is clicked, + * for use in handle_loadout_action + * - button_icon: The FontAwesome icon to display on the button + * - active_key: In the loadout UI, this key is checked in the user's loadout list for this item + * to determine if the button is 'active' (green) or not (blue). + * - active_text: Optional, if provided, the button appears to be a checkbox and this text is shown when 'active' + * - inactive_text: Optional, if provided, the button appears to be a checkbox and this text is shown when not 'active' + */ +/datum/loadout_item/proc/get_ui_buttons() as /list + SHOULD_CALL_PARENT(TRUE) + + var/list/button_list = list() + + if(can_be_greyscale) + UNTYPED_LIST_ADD(button_list, list( + "label" = "Recolor", + "act_key" = "select_color", + "button_icon" = FA_ICON_PALETTE, + "active_key" = INFO_GREYSCALE, + )) + + if(can_be_named) + UNTYPED_LIST_ADD(button_list, list( + "label" = "Rename", + "act_key" = "set_name", + "button_icon" = FA_ICON_PEN, + "active_key" = INFO_NAMED, + )) + + return button_list + +/** + * Returns a list of options this item can be reskinned into. + */ +/datum/loadout_item/proc/get_reskin_options() as /list + if(!can_be_reskinned) + return null + + var/list/reskins = list() + + for(var/skin in cached_reskin_options) + UNTYPED_LIST_ADD(reskins, list( + "name" = skin, + "tooltip" = skin, + "skin_icon_state" = cached_reskin_options[skin], + )) + + return reskins diff --git a/code/modules/loadout/loadout_menu.dm b/code/modules/loadout/loadout_menu.dm new file mode 100644 index 00000000000..243641faeed --- /dev/null +++ b/code/modules/loadout/loadout_menu.dm @@ -0,0 +1,120 @@ +/datum/preference_middleware/loadout + action_delegations = list( + "clear_all_items" = PROC_REF(action_clear_all), + "pass_to_loadout_item" = PROC_REF(action_pass_to_loadout_item), + "rotate_dummy" = PROC_REF(action_rotate_model_dir), + "select_item" = PROC_REF(action_select_item), + "toggle_job_clothes" = PROC_REF(action_toggle_job_outfit), + "close_greyscale_menu" = PROC_REF(force_close_greyscale_menu), + ) + /// Our currently open greyscaling menu. + VAR_FINAL/datum/greyscale_modify_menu/menu + +/datum/preference_middleware/loadout/Destroy(force, ...) + QDEL_NULL(menu) + return ..() + +/datum/preference_middleware/loadout/on_new_character(mob/user) + preferences.character_preview_view?.update_body() + +/datum/preference_middleware/loadout/proc/action_select_item(list/params, mob/user) + PRIVATE_PROC(TRUE) + var/path_to_use = text2path(params["path"]) + var/datum/loadout_item/interacted_item = GLOB.all_loadout_datums[path_to_use] + if(!istype(interacted_item)) + stack_trace("Failed to locate desired loadout item (path: [params["path"]]) in the global list of loadout datums!") + return TRUE // update + + if(params["deselect"]) + deselect_item(interacted_item) + else + select_item(interacted_item) + return TRUE + +/datum/preference_middleware/loadout/proc/action_clear_all(list/params, mob/user) + PRIVATE_PROC(TRUE) + preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], null) + return TRUE + +/datum/preference_middleware/loadout/proc/action_toggle_job_outfit(list/params, mob/user) + PRIVATE_PROC(TRUE) + preferences.character_preview_view.show_job_clothes = !preferences.character_preview_view.show_job_clothes + preferences.character_preview_view.update_body() + return TRUE + +/datum/preference_middleware/loadout/proc/action_rotate_model_dir(list/params, mob/user) + PRIVATE_PROC(TRUE) + switch(params["dir"]) + if("left") + preferences.character_preview_view.setDir(turn(preferences.character_preview_view.dir, -90)) + if("right") + preferences.character_preview_view.setDir(turn(preferences.character_preview_view.dir, 90)) + +/datum/preference_middleware/loadout/proc/action_pass_to_loadout_item(list/params, mob/user) + PRIVATE_PROC(TRUE) + var/path_to_use = text2path(params["path"]) + var/datum/loadout_item/interacted_item = GLOB.all_loadout_datums[path_to_use] + if(!istype(interacted_item)) // no you cannot href exploit to spawn with a pulse rifle + stack_trace("Failed to locate desired loadout item (path: [params["path"]]) in the global list of loadout datums!") + return TRUE // update + + if(interacted_item.handle_loadout_action(src, user, params["subaction"], params)) + preferences.character_preview_view.update_body() + return TRUE + + return FALSE + +/// Select [path] item to [category_slot] slot. +/datum/preference_middleware/loadout/proc/select_item(datum/loadout_item/selected_item) + var/list/loadout = preferences.read_preference(/datum/preference/loadout) + var/list/datum/loadout_item/loadout_datums = loadout_list_to_datums(loadout) + for(var/datum/loadout_item/item as anything in loadout_datums) + if(item.category != selected_item.category) + continue + if(!item.category.handle_duplicate_entires(src, item, selected_item, loadout_datums)) + return + + LAZYSET(loadout, selected_item.item_path, list()) + preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + +/// Deselect [deselected_item]. +/datum/preference_middleware/loadout/proc/deselect_item(datum/loadout_item/deselected_item) + var/list/loadout = preferences.read_preference(/datum/preference/loadout) + LAZYREMOVE(loadout, deselected_item.item_path) + preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout) + +/datum/preference_middleware/loadout/proc/register_greyscale_menu(datum/greyscale_modify_menu/open_menu) + src.menu = open_menu + RegisterSignal(menu, COMSIG_QDELETING, PROC_REF(cleanup_greyscale_menu)) + +/datum/preference_middleware/loadout/proc/cleanup_greyscale_menu() + SIGNAL_HANDLER + menu = null + +/datum/preference_middleware/loadout/proc/force_close_greyscale_menu() + menu?.ui_close() + +/datum/preference_middleware/loadout/get_ui_data(mob/user) + var/list/data = list() + data["job_clothes"] = preferences.character_preview_view.show_job_clothes + return data + +/datum/preference_middleware/loadout/get_ui_static_data(mob/user) + var/list/data = list() + data["loadout_preview_view"] = preferences.character_preview_view.assigned_map + return data + +/datum/preference_middleware/loadout/get_constant_data() + var/list/data = list() + var/list/loadout_tabs = list() + for(var/datum/loadout_category/category as anything in GLOB.all_loadout_categories) + var/list/cat_data = list( + "name" = category.category_name, + "category_icon" = category.category_ui_icon, + "category_info" = category.category_info, + "contents" = category.items_to_ui_data(), + ) + UNTYPED_LIST_ADD(loadout_tabs, cat_data) + + data["loadout_tabs"] = loadout_tabs + return data diff --git a/code/modules/loadout/loadout_preference.dm b/code/modules/loadout/loadout_preference.dm new file mode 100644 index 00000000000..c1d286bedf6 --- /dev/null +++ b/code/modules/loadout/loadout_preference.dm @@ -0,0 +1,59 @@ +/datum/preference/loadout + savefile_key = "loadout_list" + savefile_identifier = PREFERENCE_CHARACTER + priority = PREFERENCE_PRIORITY_LOADOUT + can_randomize = FALSE + // Loadout preference is an assoc list [item_path] = [loadout item information list] + // + // it may look something like + // - list(/obj/item/glasses = list()) + // or + // - list(/obj/item/plush/lizard = list("name" = "Tests-The-Loadout", "color" = "#FF0000")) + +// Loadouts are applied with job equip code. +/datum/preference/loadout/apply_to_human(mob/living/carbon/human/target, value) + return + +// Sanitize on load to ensure no invalid paths from older saves get in +/datum/preference/loadout/deserialize(input, datum/preferences/preferences) + return sanitize_loadout_list(input, preferences.parent?.mob) + +// Default value is null - the loadout list is a lazylist +/datum/preference/loadout/create_default_value(datum/preferences/preferences) + return null + +/datum/preference/loadout/is_valid(value) + return isnull(value) || islist(value) + +/** + * Removes all invalid paths from loadout lists. + * This is a general sanitization for preference loading. + * + * Returns a list, or null if empty + */ +/datum/preference/loadout/proc/sanitize_loadout_list(list/passed_list, mob/optional_loadout_owner) as /list + var/list/sanitized_list + for(var/path in passed_list) + // Loading from json has each path in the list as a string that we need to convert back to typepath + var/obj/item/real_path = istext(path) ? text2path(path) : path + if(!ispath(real_path, /obj/item)) + if(optional_loadout_owner) + to_chat(optional_loadout_owner, span_boldnotice("The following invalid item path was found \ + in your character loadout: [real_path || "null"]. \ + It has been removed, renamed, or is otherwise missing - \ + You may want to check your loadout settings.")) + continue + + else if(!istype(GLOB.all_loadout_datums[real_path], /datum/loadout_item)) + if(optional_loadout_owner) + to_chat(optional_loadout_owner, span_boldnotice("The following invalid loadout item was found \ + in your character loadout: [real_path || "null"]. \ + It has been removed, renamed, or is otherwise missing - \ + You may want to check your loadout settings.")) + continue + + // Set into sanitize list using converted path key + var/list/data = passed_list[path] + LAZYSET(sanitized_list, real_path, LAZYLISTDUPLICATE(data)) + + return sanitized_list diff --git a/code/modules/mob/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm index 3d4fb9577b6..49ac17d48ec 100644 --- a/code/modules/mob/dead/new_player/preferences_setup.dm +++ b/code/modules/mob/dead/new_player/preferences_setup.dm @@ -93,8 +93,9 @@ return preview_job -/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin) - var/datum/job/preview_job = get_highest_priority_job() +/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin, show_job_clothes = TRUE) + var/datum/job/no_job = SSjob.GetJobType(/datum/job/unassigned) + var/datum/job/preview_job = get_highest_priority_job() || no_job if(preview_job) // Silicons only need a very basic preview since there is no customization for them. @@ -106,9 +107,13 @@ // Set up the dummy for its photoshoot apply_prefs_to(mannequin, TRUE) - if(preview_job) - mannequin.job = preview_job.title - mannequin.dress_up_as_job(preview_job, TRUE) + mannequin.job = preview_job.title + mannequin.dress_up_as_job( + equipping = show_job_clothes ? preview_job : no_job, + visual_only = TRUE, + player_client = parent, + consistent = TRUE, + ) // Apply visual quirks // Yes we do it every time because it needs to be done after job gear diff --git a/icons/obj/fluff/previews.dmi b/icons/obj/fluff/previews.dmi index c62b055f71efdd4ad3004a609435699c244944a1..8a69c1363189db811fd877a9368310c8aee58591 100644 GIT binary patch literal 8491 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 BirN4G literal 7799 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 diff --git a/tgstation.dme b/tgstation.dme index f2337e2cb7f..71bdb4e69fa 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1727,7 +1727,6 @@ #include "code\datums\quirks\neutral_quirks\photographer.dm" #include "code\datums\quirks\neutral_quirks\pineapple_hater.dm" #include "code\datums\quirks\neutral_quirks\pineapple_liker.dm" -#include "code\datums\quirks\neutral_quirks\pride_pin.dm" #include "code\datums\quirks\neutral_quirks\shifty_eyes.dm" #include "code\datums\quirks\neutral_quirks\snob.dm" #include "code\datums\quirks\neutral_quirks\transhumanist.dm" @@ -3695,7 +3694,6 @@ #include "code\modules\client\preferences\pixel_size.dm" #include "code\modules\client\preferences\playtime_reward_cloak.dm" #include "code\modules\client\preferences\preferred_map.dm" -#include "code\modules\client\preferences\pride_pin.dm" #include "code\modules\client\preferences\prisoner_crime.dm" #include "code\modules\client\preferences\prosthetic_limb.dm" #include "code\modules\client\preferences\prosthetic_organ.dm" @@ -3730,6 +3728,7 @@ #include "code\modules\client\preferences\migrations\body_type_migration.dm" #include "code\modules\client\preferences\migrations\convert_to_json_savefile.dm" #include "code\modules\client\preferences\migrations\legacy_sound_toggles_migration.dm" +#include "code\modules\client\preferences\migrations\quirk_loadout_migration.dm" #include "code\modules\client\preferences\migrations\tgui_prefs_migration.dm" #include "code\modules\client\preferences\migrations\tts_blip_migration.dm" #include "code\modules\client\preferences\species_features\basic.dm" @@ -4403,6 +4402,17 @@ #include "code\modules\lighting\lighting_source.dm" #include "code\modules\lighting\lighting_turf.dm" #include "code\modules\lighting\static_lighting_area.dm" +#include "code\modules\loadout\loadout_categories.dm" +#include "code\modules\loadout\loadout_helpers.dm" +#include "code\modules\loadout\loadout_items.dm" +#include "code\modules\loadout\loadout_menu.dm" +#include "code\modules\loadout\loadout_preference.dm" +#include "code\modules\loadout\categories\accessories.dm" +#include "code\modules\loadout\categories\glasses.dm" +#include "code\modules\loadout\categories\heads.dm" +#include "code\modules\loadout\categories\inhands.dm" +#include "code\modules\loadout\categories\neck.dm" +#include "code\modules\loadout\categories\pocket.dm" #include "code\modules\logging\log_category.dm" #include "code\modules\logging\log_entry.dm" #include "code\modules\logging\log_holder.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx index 261c40408f1..1e6f779c847 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx @@ -7,6 +7,7 @@ import { Window } from '../../layouts'; import { AntagsPage } from './AntagsPage'; import { PreferencesMenuData } from './data'; import { JobsPage } from './JobsPage'; +import { LoadoutPage } from './loadout/index'; import { MainPage } from './MainPage'; import { PageButton } from './PageButton'; import { QuirksPage } from './QuirksPage'; @@ -18,6 +19,7 @@ enum Page { Jobs, Species, Quirks, + Loadout, } const CharacterProfiles = (props: { @@ -75,6 +77,11 @@ export const CharacterPreferenceWindow = (props) => { case Page.Quirks: pageContents = ; break; + + case Page.Loadout: + pageContents = ; + break; + default: exhaustiveCheck(currentPage); } @@ -116,6 +123,16 @@ export const CharacterPreferenceWindow = (props) => { + + + Loadout + + + ; @@ -191,6 +194,9 @@ export type ServerData = { random: { randomizable: string[]; }; + loadout: { + loadout_tabs: LoadoutCategory[]; + }; species: Record; [otheyKey: string]: unknown; }; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ItemDisplay.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ItemDisplay.tsx new file mode 100644 index 00000000000..11b20b00979 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ItemDisplay.tsx @@ -0,0 +1,147 @@ +import { createSearch } from '../../../../common/string'; +import { useBackend } from '../../../backend'; +import { + Box, + Button, + DmIcon, + Flex, + Icon, + NoticeBox, +} from '../../../components'; +import { LoadoutCategory, LoadoutItem, LoadoutManagerData } from './base'; + +export const ItemIcon = (props: { item: LoadoutItem; scale?: number }) => { + const { item, scale = 3 } = props; + const icon_to_use = item.icon; + const icon_state_to_use = item.icon_state; + + if (!icon_to_use || !icon_state_to_use) { + return ( + + ); + } + + return ( + } + icon={icon_to_use} + icon_state={icon_state_to_use} + style={{ + transform: `scale(${scale}) translateX(${scale * 3}px) translateY(${ + scale * 3 + }px)`, + }} + /> + ); +}; + +export const ItemDisplay = (props: { + active: boolean; + item: LoadoutItem; + scale?: number; +}) => { + const { act } = useBackend(); + const { active, item, scale = 3 } = props; + + const boxSize = `${scale * 32}px`; + + return ( + + ); +}; + +const ItemListDisplay = (props: { items: LoadoutItem[] }) => { + const { data } = useBackend(); + const { loadout_list } = data.character_preferences.misc; + return ( + + {props.items.map((item) => ( + + + + ))} + + ); +}; + +export const LoadoutTabDisplay = (props: { + category: LoadoutCategory | undefined; +}) => { + const { category } = props; + if (!category) { + return ( + + Erroneous category detected! This is a bug, please report it. + + ); + } + + return ; +}; + +export const SearchDisplay = (props: { + loadout_tabs: LoadoutCategory[]; + currentSearch: string; +}) => { + const { loadout_tabs, currentSearch } = props; + + const search = createSearch( + currentSearch, + (loadout_item: LoadoutItem) => loadout_item.name, + ); + + const validLoadoutItems = loadout_tabs + .flatMap((tab) => tab.contents) + .filter(search) + .sort((a, b) => (a.name > b.name ? 1 : -1)); + + if (validLoadoutItems.length === 0) { + return No items found!; + } + + return ; +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ModifyPanel.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ModifyPanel.tsx new file mode 100644 index 00000000000..70959691af8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/ModifyPanel.tsx @@ -0,0 +1,219 @@ +import { useBackend } from '../../../backend'; +import { + Box, + Button, + Dimmer, + DmIcon, + Flex, + Icon, + LabeledList, + Section, + Stack, +} from '../../../components'; +import { FAIcon, LoadoutItem, LoadoutManagerData, ReskinOption } from './base'; +import { ItemIcon } from './ItemDisplay'; + +// Used in LoadoutItem to make buttons relating to how an item can be edited +export type LoadoutButton = { + label: string; + act_key?: string; + button_icon?: FAIcon; + button_text?: string; + active_key?: string; + active_text?: string; + inactive_text?: string; + tooltip_text?: string; +}; + +const LoadoutModifyButton = (props: { + button: LoadoutButton; + modifyItemDimmer: LoadoutItem; +}) => { + const { act, data } = useBackend(); + const { loadout_list } = data.character_preferences.misc; + const { button, modifyItemDimmer } = props; + + const buttonIsActive = + button.active_key && loadout_list[modifyItemDimmer.path][button.active_key]; + + if (button.active_text && button.inactive_text) { + return ( + { + act('pass_to_loadout_item', { + subaction: button.act_key, + path: modifyItemDimmer.path, + }); + }} + > + {buttonIsActive ? button.active_text : button.inactive_text} + + ); + } + + return ( + + ); +}; + +const LoadoutModifyButtons = (props: { modifyItemDimmer: LoadoutItem }) => { + const { act, data } = useBackend(); + const { loadout_list } = data.character_preferences.misc; + const { modifyItemDimmer } = props; + + const isActive = (item: LoadoutItem, reskin: ReskinOption) => { + return loadout_list && loadout_list[item.path]['reskin'] + ? loadout_list[item.path]['reskin'] === reskin.name + : item.icon_state === reskin.skin_icon_state; + }; + + return ( + + + + {!!modifyItemDimmer.reskins && ( + + + {modifyItemDimmer.reskins.map((reskin) => ( + + + + ))} + + + )} + {modifyItemDimmer.buttons.map((button) => ( + + + + ))} + + + + ); +}; + +const LoadoutModifyItemDisplay = (props: { modifyItemDimmer: LoadoutItem }) => { + const { modifyItemDimmer } = props; + return ( + + + + {modifyItemDimmer.name} + + + + + + + ); +}; + +export const LoadoutModifyDimmer = (props: { + modifyItemDimmer: LoadoutItem; + setModifyItemDimmer: (dimmer: LoadoutItem | null) => void; +}) => { + const { act } = useBackend(); + const { modifyItemDimmer, setModifyItemDimmer } = props; + return ( + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/base.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/base.ts new file mode 100644 index 00000000000..33f8cabea9c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/base.ts @@ -0,0 +1,46 @@ +import { BooleanLike } from '../../../../common/react'; +import { PreferencesMenuData } from '../data'; +import { LoadoutButton } from './ModifyPanel'; + +// Generic types +export type DmIconFile = string; +export type DmIconState = string; +export type FAIcon = string; +export type typePath = string; + +type LoadoutInfoKey = string; +type LoadoutInfoValue = string; +// Info about a loadout item (key to info, such as color, reskin, layer, etc) +type LoadoutListInfo = Record | []; +// Typepath to info about the item +export type LoadoutList = Record; + +// Used for holding reskin information +export type ReskinOption = { + name: string; + tooltip: string; + skin_icon_state: DmIconState; // The icon is the same as the item icon +}; + +// Actual item passed in from the loadout +export type LoadoutItem = { + name: string; + path: typePath; + icon: DmIconFile | null; + icon_state: DmIconState | null; + buttons: LoadoutButton[]; + reskins: ReskinOption[] | null; + information: string[]; +}; + +// Category of items in the loadout +export type LoadoutCategory = { + name: string; + category_icon: FAIcon | null; + category_info: string | null; + contents: LoadoutItem[]; +}; + +export type LoadoutManagerData = PreferencesMenuData & { + job_clothes: BooleanLike; +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/index.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/index.tsx new file mode 100644 index 00000000000..211f8ed8ae5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/loadout/index.tsx @@ -0,0 +1,337 @@ +import { Fragment, useState } from 'react'; + +import { useBackend } from '../../../backend'; +import { + Box, + Button, + Divider, + Icon, + Input, + NoticeBox, + Section, + Stack, + Tabs, +} from '../../../components'; +import { CharacterPreview } from '../../common/CharacterPreview'; +import { ServerData } from '../data'; +import { ServerPreferencesFetcher } from '../ServerPreferencesFetcher'; +import { + LoadoutCategory, + LoadoutItem, + LoadoutManagerData, + typePath, +} from './base'; +import { ItemIcon, LoadoutTabDisplay, SearchDisplay } from './ItemDisplay'; +import { LoadoutModifyDimmer } from './ModifyPanel'; + +export const LoadoutPage = () => { + return ( + { + if (!serverData) { + return Loading...; + } + const loadoutServerData: ServerData = serverData; + return ( + + ); + }} + /> + ); +}; + +const LoadoutPageInner = (props: { loadout_tabs: LoadoutCategory[] }) => { + const { loadout_tabs } = props; + const [searchLoadout, setSearchLoadout] = useState(''); + const [selectedTabName, setSelectedTab] = useState(loadout_tabs[0].name); + const [modifyItemDimmer, setModifyItemDimmer] = useState( + null, + ); + + return ( + + + {!!modifyItemDimmer && ( + + )} +
setSearchLoadout(value)} + placeholder="Search for an item..." + value={searchLoadout} + /> + } + > + + {loadout_tabs.map((curTab) => ( + { + setSelectedTab(curTab.name); + setSearchLoadout(''); + }} + > + + {curTab.category_icon && ( + + )} + {curTab.name} + + + ))} + +
+
+ + + +
+ ); +}; + +const LoadoutTabs = (props: { + loadout_tabs: LoadoutCategory[]; + currentTab: string; + currentSearch: string; + modifyItemDimmer: LoadoutItem | null; + setModifyItemDimmer: (dimmer: LoadoutItem | null) => void; +}) => { + const { + loadout_tabs, + currentTab, + currentSearch, + modifyItemDimmer, + setModifyItemDimmer, + } = props; + const activeCategory = loadout_tabs.find((curTab) => { + return curTab.name === currentTab; + }); + const searching = currentSearch.length > 1; + + return ( + + + + + + + + + + + + + {searching || activeCategory?.contents ? ( +
+ {activeCategory.category_info} + + ) : null + } + > + + + {searching ? ( + + ) : ( + + )} + + +
+ ) : ( +
+ No contents for selected tab. +
+ )} +
+
+ ); +}; + +const typepathToLoadoutItem = ( + typepath: typePath, + all_tabs: LoadoutCategory[], +) => { + // Maybe a bit inefficient, could be replaced with a hashmap? + for (const tab of all_tabs) { + for (const item of tab.contents) { + if (item.path === typepath) { + return item; + } + } + } + return null; +}; + +const LoadoutSelectedItem = (props: { + path: typePath; + all_tabs: LoadoutCategory[]; + modifyItemDimmer: LoadoutItem | null; + setModifyItemDimmer: (dimmer: LoadoutItem | null) => void; +}) => { + const { all_tabs, path, modifyItemDimmer, setModifyItemDimmer } = props; + const { act } = useBackend(); + + const item = typepathToLoadoutItem(path, all_tabs); + if (!item) { + return null; + } + + return ( + + + + + {item.name} + {item.buttons.length ? ( + + + + ) : ( + // empty space + )} + + + + + ); +}; + +const LoadoutSelectedSection = (props: { + all_tabs: LoadoutCategory[]; + modifyItemDimmer: LoadoutItem | null; + setModifyItemDimmer: (dimmer: LoadoutItem | null) => void; +}) => { + const { act, data } = useBackend(); + const { loadout_list } = data.character_preferences.misc; + const { all_tabs, modifyItemDimmer, setModifyItemDimmer } = props; + + return ( +
act('clear_all_items')} + > + Clear All + + } + > + {loadout_list && + Object.entries(loadout_list).map(([path, item]) => ( + + + + + ))} +
+ ); +}; + +const LoadoutPreviewSection = () => { + const { act, data } = useBackend(); + + return ( +
act('toggle_job_clothes')} + > + Job Clothes + + } + > + + + + + + + + +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pride_pin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pride_pin.tsx deleted file mode 100644 index 70c6f8a8efe..00000000000 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/pride_pin.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { FeatureChoiced } from '../base'; -import { FeatureDropdownInput } from '../dropdowns'; - -export const pride_pin: FeatureChoiced = { - name: 'Pride Pin', - component: FeatureDropdownInput, -};