From 3893e836d5aaeb837c7b493ece1c32d12f0730ad Mon Sep 17 00:00:00 2001 From: Iajret Creature <122297233+AnArgonianLizardThatStealsPRs@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:21:20 +0300 Subject: [PATCH] [MIRROR] Adds a little button to quirks that allows for relatively easy customization [MDB IGNORE] (#600) * Adds a little button to quirks that allows for relatively easy customization * Fixing diffs --------- Co-authored-by: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Co-authored-by: nikothedude <59709059+nikothedude@users.noreply.github.com> Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com> --- code/__DEFINES/preferences.dm | 3 + code/datums/quirks/_quirk_constant_data.dm | 72 ++++++++++ .../quirks/negative_quirks/food_allergy.dm | 4 + .../quirks/negative_quirks/nearsighted.dm | 4 + .../quirks/negative_quirks/prosthetic_limb.dm | 4 + code/datums/quirks/neutral_quirks/phobia.dm | 4 + .../quirks/positive_quirks/bilingual.dm | 4 + code/datums/quirks/positive_quirks/tagger.dm | 4 + code/modules/client/preferences.dm | 4 +- .../client/preferences/food_allergy.dm | 2 +- code/modules/client/preferences/glasses.dm | 2 +- code/modules/client/preferences/language.dm | 2 +- .../client/preferences/middleware/quirks.dm | 5 + .../modules/client/preferences/paint_color.dm | 2 +- code/modules/client/preferences/phobia.dm | 2 +- code/modules/client/preferences/prosthetic.dm | 2 +- tgstation.dme | 1 + .../interfaces/PreferencesMenu/MainPage.tsx | 90 ++++++++----- .../interfaces/PreferencesMenu/QuirksPage.tsx | 125 +++++++++++++++++- .../tgui/interfaces/PreferencesMenu/data.ts | 3 + 20 files changed, 294 insertions(+), 45 deletions(-) create mode 100644 code/datums/quirks/_quirk_constant_data.dm diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 6b34d064406..fa24963e34e 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -132,6 +132,9 @@ /// such as hair color being affixed to hair. #define PREFERENCE_CATEGORY_SUPPLEMENTAL_FEATURES "supplemental_features" +/// These preferences will not be rendered on the preferences page, and are practically invisible unless specifically rendered. Used for quirks, currently. +#define PREFERENCE_CATEGORY_MANUALLY_RENDERED "manually_rendered_features" + // Playtime is tracked in minutes /// The time needed to unlock hardcore random mode in preferences #define PLAYTIME_HARDCORE_RANDOM 120 // 2 hours diff --git a/code/datums/quirks/_quirk_constant_data.dm b/code/datums/quirks/_quirk_constant_data.dm new file mode 100644 index 00000000000..977c52a6837 --- /dev/null +++ b/code/datums/quirks/_quirk_constant_data.dm @@ -0,0 +1,72 @@ +GLOBAL_LIST_INIT_TYPED(all_quirk_constant_data, /datum/quirk_constant_data, generate_quirk_constant_data()) + +/// Constructs [GLOB.all_quirk_constant_data] by iterating through a typecache of pregen data, ignoring abstract types, and instantiating the rest. +/proc/generate_quirk_constant_data() + RETURN_TYPE(/list/datum/quirk_constant_data) + + var/list/datum/quirk_constant_data/all_constant_data = list() + + for (var/datum/quirk_constant_data/iterated_path as anything in typecacheof(path = /datum/quirk_constant_data, ignore_root_path = TRUE)) + if (initial(iterated_path.abstract_type) == iterated_path) + continue + + if (!isnull(all_constant_data[initial(iterated_path.associated_typepath)])) + stack_trace("pre-existing pregen data for [initial(iterated_path.associated_typepath)] when [iterated_path] was being considered: [all_constant_data[initial(iterated_path.associated_typepath)]]. \ + this is definitely a bug, and is probably because one of the two pregen data have the wrong quirk typepath defined. [iterated_path] will not be instantiated") + + continue + + var/datum/quirk_constant_data/pregen_data = new iterated_path + all_constant_data[pregen_data.associated_typepath] = pregen_data + + return all_constant_data + +/// A singleton datum representing constant data and procs used by quirks. +/datum/quirk_constant_data + /// Abstract in OOP terms. If this is our type, we will not be instantiated. + var/abstract_type = /datum/quirk_constant_data + + /// The typepath of the quirk we will be associated with in the global list. This is what we represent. + var/datum/quirk/associated_typepath + + /// A lazylist of preference datum typepaths. Any character pref put in here will be rendered in the quirks page under a dropdown. + var/list/datum/preference/customization_options + +/datum/quirk_constant_data/New() + . = ..() + + ASSERT(abstract_type != type && !isnull(associated_typepath), "associated_typepath null - please set it! occured on: [src.type]") + +/// Returns a list of savefile_keys derived from the preference typepaths in [customization_options]. Used in quirks middleware to supply the preferences to render. +/datum/quirk_constant_data/proc/get_customization_data() + RETURN_TYPE(/list) + + var/list/customization_data = list() + + for (var/datum/preference/pref_type as anything in customization_options) + var/datum/preference/pref_instance = GLOB.preference_entries[pref_type] + if (isnull(pref_instance)) + stack_trace("get_customization_data was called before instantiation of [pref_type]!") + continue // just in case its a fluke and its only this one thats not instantiated, we'll check the other pref entries + + customization_data += pref_instance.savefile_key + + return customization_data + +/// Is this quirk customizable? If true, a button will appear within the quirk's description box in the quirks page, and upon clicking it, +/// will open a customization menu for the quirk. +/datum/quirk_constant_data/proc/is_customizable() + return LAZYLEN(customization_options) > 0 + +/datum/quirk_constant_data/Destroy(force, ...) + var/error_message = "[src], a singleton quirk constant data instance, was destroyed! This should not happen!" + if (force) + error_message += " NOTE: This Destroy() was called with force == TRUE. This instance will be deleted and replaced with a new one." + stack_trace(error_message) + + if (!force) + return QDEL_HINT_LETMELIVE + + . = ..() + + GLOB.all_quirk_constant_data[associated_typepath] = new src.type //recover diff --git a/code/datums/quirks/negative_quirks/food_allergy.dm b/code/datums/quirks/negative_quirks/food_allergy.dm index c2f4eae4d0e..0b81dece085 100644 --- a/code/datums/quirks/negative_quirks/food_allergy.dm +++ b/code/datums/quirks/negative_quirks/food_allergy.dm @@ -25,6 +25,10 @@ GLOBAL_LIST_INIT(possible_food_allergies, list( /// Footype flags that will trigger the allergy var/target_foodtypes = NONE +/datum/quirk_constant_data/food_allergy + associated_typepath = /datum/quirk/item_quirk/food_allergic + customization_options = list(/datum/preference/choiced/food_allergy) + /datum/quirk/item_quirk/food_allergic/add(client/client_source) if(target_foodtypes != NONE) // Already set, don't care return diff --git a/code/datums/quirks/negative_quirks/nearsighted.dm b/code/datums/quirks/negative_quirks/nearsighted.dm index 6a5397b6504..452971a7ad0 100644 --- a/code/datums/quirks/negative_quirks/nearsighted.dm +++ b/code/datums/quirks/negative_quirks/nearsighted.dm @@ -10,6 +10,10 @@ quirk_flags = QUIRK_HUMAN_ONLY|QUIRK_CHANGES_APPEARANCE mail_goodies = list(/obj/item/clothing/glasses/regular) // extra pair if orginal one gets broken by somebody mean +/datum/quirk_constant_data/nearsighted + associated_typepath = /datum/quirk/item_quirk/nearsighted + customization_options = list(/datum/preference/choiced/glasses) + /datum/quirk/item_quirk/nearsighted/add_unique(client/client_source) var/glasses_name = client_source?.prefs.read_preference(/datum/preference/choiced/glasses) || "Regular" var/obj/item/clothing/glasses/glasses_type diff --git a/code/datums/quirks/negative_quirks/prosthetic_limb.dm b/code/datums/quirks/negative_quirks/prosthetic_limb.dm index e7ea4d75788..791837b04d9 100644 --- a/code/datums/quirks/negative_quirks/prosthetic_limb.dm +++ b/code/datums/quirks/negative_quirks/prosthetic_limb.dm @@ -11,6 +11,10 @@ /// the original limb from before the prosthetic was applied var/obj/item/bodypart/old_limb +/datum/quirk_constant_data/prosthetic_limb + associated_typepath = /datum/quirk/prosthetic_limb + customization_options = list(/datum/preference/choiced/prosthetic) + /datum/quirk/prosthetic_limb/add_unique(client/client_source) var/limb_type = GLOB.limb_choice[client_source?.prefs?.read_preference(/datum/preference/choiced/prosthetic)] if(isnull(limb_type)) //Client gone or they chose a random prosthetic diff --git a/code/datums/quirks/neutral_quirks/phobia.dm b/code/datums/quirks/neutral_quirks/phobia.dm index 224401f0670..0375c69b522 100644 --- a/code/datums/quirks/neutral_quirks/phobia.dm +++ b/code/datums/quirks/neutral_quirks/phobia.dm @@ -6,6 +6,10 @@ medical_record_text = "Patient has an irrational fear of something." mail_goodies = list(/obj/item/clothing/glasses/blindfold, /obj/item/storage/pill_bottle/psicodine) +/datum/quirk_constant_data/phobia + associated_typepath = /datum/quirk/phobia + customization_options = list(/datum/preference/choiced/phobia) + // Phobia will follow you between transfers /datum/quirk/phobia/add(client/client_source) var/phobia = client_source?.prefs.read_preference(/datum/preference/choiced/phobia) diff --git a/code/datums/quirks/positive_quirks/bilingual.dm b/code/datums/quirks/positive_quirks/bilingual.dm index 324054198b8..408a952cfe1 100644 --- a/code/datums/quirks/positive_quirks/bilingual.dm +++ b/code/datums/quirks/positive_quirks/bilingual.dm @@ -8,6 +8,10 @@ medical_record_text = "Patient speaks multiple languages." mail_goodies = list(/obj/item/taperecorder, /obj/item/clothing/head/frenchberet, /obj/item/clothing/mask/fakemoustache/italian) +/datum/quirk_constant_data/bilingual + associated_typepath = /datum/quirk/bilingual + customization_options = list(/datum/preference/choiced/language) + /datum/quirk/bilingual/add_unique(client/client_source) var/wanted_language = client_source?.prefs.read_preference(/datum/preference/choiced/language) var/datum/language/language_type diff --git a/code/datums/quirks/positive_quirks/tagger.dm b/code/datums/quirks/positive_quirks/tagger.dm index 5aba24d850a..4b0f48a1ca8 100644 --- a/code/datums/quirks/positive_quirks/tagger.dm +++ b/code/datums/quirks/positive_quirks/tagger.dm @@ -14,6 +14,10 @@ /obj/item/canvas/twentythree_twentythree ) +/datum/quirk_constant_data/tagger + associated_typepath = /datum/quirk/item_quirk/tagger + customization_options = list(/datum/preference/color/paint_color) + /datum/quirk/item_quirk/tagger/add_unique(client/client_source) var/obj/item/toy/crayon/spraycan/can = new can.set_painting_tool_color(client_source?.prefs.read_preference(/datum/preference/color/paint_color)) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 239ab09d1a7..158ed2fdbfa 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -409,13 +409,13 @@ GLOBAL_LIST_EMPTY(preferences_datums) if (!preference.is_accessible(src)) continue - LAZYINITLIST(preferences[preference.category]) - var/value = read_preference(preference.type) var/data = preference.compile_ui_data(user, value) + LAZYINITLIST(preferences[preference.category]) preferences[preference.category][preference.savefile_key] = data + for (var/datum/preference_middleware/preference_middleware as anything in middleware) var/list/append_character_preferences = preference_middleware.get_character_preferences(user) if (isnull(append_character_preferences)) diff --git a/code/modules/client/preferences/food_allergy.dm b/code/modules/client/preferences/food_allergy.dm index 461c3b31e2a..1096ef3c183 100644 --- a/code/modules/client/preferences/food_allergy.dm +++ b/code/modules/client/preferences/food_allergy.dm @@ -1,5 +1,5 @@ /datum/preference/choiced/food_allergy - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "food_allergy" savefile_identifier = PREFERENCE_CHARACTER can_randomize = FALSE diff --git a/code/modules/client/preferences/glasses.dm b/code/modules/client/preferences/glasses.dm index e34cdd0e090..03c975abce7 100644 --- a/code/modules/client/preferences/glasses.dm +++ b/code/modules/client/preferences/glasses.dm @@ -1,5 +1,5 @@ /datum/preference/choiced/glasses - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "glasses" savefile_identifier = PREFERENCE_CHARACTER should_generate_icons = TRUE diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm index 2de14be09cf..f602d6b3a66 100644 --- a/code/modules/client/preferences/language.dm +++ b/code/modules/client/preferences/language.dm @@ -1,5 +1,5 @@ /datum/preference/choiced/language - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "language" savefile_identifier = PREFERENCE_CHARACTER diff --git a/code/modules/client/preferences/middleware/quirks.dm b/code/modules/client/preferences/middleware/quirks.dm index db1817276b4..18ddf0aebab 100644 --- a/code/modules/client/preferences/middleware/quirks.dm +++ b/code/modules/client/preferences/middleware/quirks.dm @@ -33,11 +33,16 @@ for (var/quirk_name in quirks) var/datum/quirk/quirk = quirks[quirk_name] + var/datum/quirk_constant_data/constant_data = GLOB.all_quirk_constant_data[quirk] + var/list/datum/preference/customization_options = constant_data?.get_customization_data() + quirk_info[sanitize_css_class_name(quirk_name)] = list( "description" = initial(quirk.desc), "icon" = initial(quirk.icon), "name" = quirk_name, "value" = initial(quirk.value), + "customizable" = constant_data?.is_customizable(), + "customization_options" = customization_options, "veteran_only" = initial(quirk.veteran_only), // SKYRAT EDIT - Veteran quirks ) diff --git a/code/modules/client/preferences/paint_color.dm b/code/modules/client/preferences/paint_color.dm index a44208b8325..749b83cf000 100644 --- a/code/modules/client/preferences/paint_color.dm +++ b/code/modules/client/preferences/paint_color.dm @@ -2,7 +2,7 @@ /datum/preference/color/paint_color savefile_key = "paint_color" savefile_identifier = PREFERENCE_CHARACTER - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED /datum/preference/color/paint_color/is_accessible(datum/preferences/preferences) if (!..(preferences)) diff --git a/code/modules/client/preferences/phobia.dm b/code/modules/client/preferences/phobia.dm index 2a5caa5ba0f..92004418156 100644 --- a/code/modules/client/preferences/phobia.dm +++ b/code/modules/client/preferences/phobia.dm @@ -1,5 +1,5 @@ /datum/preference/choiced/phobia - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "phobia" savefile_identifier = PREFERENCE_CHARACTER diff --git a/code/modules/client/preferences/prosthetic.dm b/code/modules/client/preferences/prosthetic.dm index f66f1278c48..a4d5b5a577b 100644 --- a/code/modules/client/preferences/prosthetic.dm +++ b/code/modules/client/preferences/prosthetic.dm @@ -1,5 +1,5 @@ /datum/preference/choiced/prosthetic - category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "prosthetic" savefile_identifier = PREFERENCE_CHARACTER diff --git a/tgstation.dme b/tgstation.dme index 99e9a3bb06b..a38c4ed34b8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1681,6 +1681,7 @@ #include "code\datums\proximity_monitor\fields\projectile_dampener.dm" #include "code\datums\proximity_monitor\fields\timestop.dm" #include "code\datums\quirks\_quirk.dm" +#include "code\datums\quirks\_quirk_constant_data.dm" #include "code\datums\quirks\negative_quirks\allergic.dm" #include "code\datums\quirks\negative_quirks\bad_back.dm" #include "code\datums\quirks\negative_quirks\bad_touch.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx index 2c9c8455856..ee64b389da8 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx @@ -1,7 +1,7 @@ import { classes } from 'common/react'; import { sendAct, useBackend, useLocalState } from '../../backend'; -import { Autofocus, Box, Button, Flex, LabeledList, Popper, Stack, TrackOutsideClicks, Dropdown } from '../../components'; // SKYRAT EDIT CHANGE -import { createSetPreference, PreferencesMenuData, RandomSetting } from './data'; +import { Autofocus, Box, Button, Flex, LabeledList, Popper, Stack, TrackOutsideClicks, Dropdown } from '../../components'; // SKYRAT EDIT CHANGE - Adds Dropdown +import { createSetPreference, PreferencesMenuData, RandomSetting, ServerData } from './data'; import { CharacterPreview } from '../common/CharacterPreview'; import { RandomizationButton } from './RandomizationButton'; import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; @@ -358,10 +358,11 @@ const sortPreferences = sortBy<[string, unknown]>(([featureId, _]) => { return feature?.name; }); -const PreferenceList = (props: { +export const PreferenceList = (props: { act: typeof sendAct; preferences: Record; randomizations: Record; + maxHeight: string; }) => { return ( + overflowY="auto" + maxHeight={props.maxHeight}> {sortPreferences(Object.entries(props.preferences)).map( ([featureId, value]) => { @@ -421,6 +423,37 @@ const PreferenceList = (props: { ); }; +export const getRandomization = ( + preferences: Record, + serverData: ServerData | undefined, + randomBodyEnabled: boolean, + context +): Record => { + if (!serverData) { + return {}; + } + + const { data } = useBackend(context); + + return Object.fromEntries( + filterMap(Object.keys(preferences), (preferenceKey) => { + if (serverData.random.randomizable.indexOf(preferenceKey) === -1) { + return undefined; + } + + if (!randomBodyEnabled) { + return undefined; + } + + return [ + preferenceKey, + data.character_preferences.randomization[preferenceKey] || + RandomSetting.Disabled, + ]; + }) + ); +}; + export const MainPage = ( props: { openSpecies: () => void; @@ -467,36 +500,11 @@ export const MainPage = ( data.character_preferences.non_contextual.random_body !== RandomSetting.Disabled || randomToggleEnabled; - const getRandomization = ( - preferences: Record - ): Record => { - if (!serverData) { - return {}; - } - - return Object.fromEntries( - filterMap(Object.keys(preferences), (preferenceKey) => { - if ( - serverData.random.randomizable.indexOf(preferenceKey) === -1 - ) { - return undefined; - } - - if (!randomBodyEnabled) { - return undefined; - } - - return [ - preferenceKey, - data.character_preferences.randomization[preferenceKey] || - RandomSetting.Disabled, - ]; - }) - ); - }; - const randomizationOfMainFeatures = getRandomization( - Object.fromEntries(mainFeatures) + Object.fromEntries(mainFeatures), + serverData, + randomBodyEnabled, + context ); const nonContextualPreferences = { @@ -631,14 +639,26 @@ export const MainPage = ( diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx index b08e372a9d0..861e501c363 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx @@ -1,8 +1,11 @@ import { StatelessComponent } from 'inferno'; -import { Box, Icon, Stack, Tooltip } from '../../components'; -import { PreferencesMenuData, Quirk } from './data'; +import { Box, Button, Icon, Popper, Stack, Tooltip } from '../../components'; +import { PreferencesMenuData, Quirk, RandomSetting, ServerData } from './data'; import { useBackend, useLocalState } from '../../backend'; import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; +import { filterMap } from 'common/collections'; +import { getRandomization, PreferenceList } from './MainPage'; +import { useRandomToggleState } from './useRandomToggleState'; const getValueClass = (value: number): string => { if (value > 0) { @@ -14,6 +17,21 @@ const getValueClass = (value: number): string => { } }; +const getCorrespondingPreferences = ( + customization_options: string[], + relevant_preferences: Record +): Record => { + return Object.fromEntries( + filterMap(Object.keys(relevant_preferences), (key) => { + if (!customization_options.includes(key)) { + return undefined; + } + + return [key, relevant_preferences[key]]; + }) + ); +}; + const QuirkList = (props: { quirks: [ string, @@ -22,13 +40,33 @@ const QuirkList = (props: { } ][]; onClick: (quirkName: string, quirk: Quirk) => void; + selected: boolean; + serverData: ServerData; + randomBodyEnabled: boolean; + context; }) => { + const { act, data } = useBackend(props.context); + return ( // Stack is not used here for a variety of IE flex bugs {props.quirks.map(([quirkKey, quirk]) => { + const [customizationExpanded, setCustomizationExpanded] = + useLocalState( + props.context, + quirk.name + ' customization', + false + ); + const className = 'PreferencesMenu__Quirks__QuirkList__quirk'; + const hasExpandableCustomization = + quirk.customizable && + props.selected && + customizationExpanded && + quirk.customization_options && + Object.entries(quirk.customization_options).length > 0; + const child = ( { + if (props.selected) { + setCustomizationExpanded(false); + } props.onClick(quirkKey, quirk); }}> @@ -95,6 +136,72 @@ const QuirkList = (props: { 'padding': '3px', }}> {quirk.description} + {!!quirk.customizable && ( + + {!!quirk.customization_options && + hasExpandableCustomization && ( + + { + e.stopPropagation(); + }} + maxWidth="300px" + backgroundColor="black" + px="5px" + py="3px"> + + + + + + )} + + }> + {props.selected && ( +