diff --git a/code/__DEFINES/~skyrat_defines/_globalvars/food.dm b/code/__DEFINES/~skyrat_defines/_globalvars/food.dm new file mode 100644 index 00000000000..776623bda0a --- /dev/null +++ b/code/__DEFINES/~skyrat_defines/_globalvars/food.dm @@ -0,0 +1,54 @@ +/// A mirror of the FOOD_FLAGS_IC define, except with bitflags as values and as a GLOB. +GLOBAL_LIST_INIT(food_ic_flag_to_bitflag, list( + "Meat" = MEAT, + "Vegetables" = VEGETABLES, + "Raw food" = RAW, + "Junk food" = JUNKFOOD, + "Grain" = GRAIN, + "Fruits" = FRUIT, + "Dairy products" = DAIRY, + "Fried food" = FRIED, + "Alcohol" = ALCOHOL, + "Sugary food" = SUGAR, + "Gross food" = GROSS, + "Toxic food" = TOXIC, + "Pineapples" = PINEAPPLE, // I know why this is a food type, but this is pointless to the Nth degree... + "Breakfast food" = BREAKFAST, + "Clothing" = CLOTH, + "Nuts" = NUTS, + "Seafood" = SEAFOOD, + "Oranges" = ORANGES, // Why is this even a foodtype?! + "Bugs" = BUGS, + "Gore" = GORE, + "Bloody" = BLOODY, +)) + +GLOBAL_LIST_INIT(food_defaults, list( + "Meat" = FOOD_PREFERENCE_NEUTRAL, + "Vegetables" = FOOD_PREFERENCE_NEUTRAL, + "Raw food" = FOOD_PREFERENCE_DISLIKED, + "Junk food" = FOOD_PREFERENCE_NEUTRAL, + "Grain" = FOOD_PREFERENCE_NEUTRAL, + "Fruits" = FOOD_PREFERENCE_NEUTRAL, + "Dairy products" = FOOD_PREFERENCE_NEUTRAL, + "Fried food" = FOOD_PREFERENCE_NEUTRAL, + "Alcohol" = FOOD_PREFERENCE_DISLIKED, + "Sugary food" = FOOD_PREFERENCE_NEUTRAL, + "Gross food" = FOOD_PREFERENCE_DISLIKED, + "Toxic food" = FOOD_PREFERENCE_TOXIC, + "Pineapples" = FOOD_PREFERENCE_NEUTRAL, + "Breakfast food" = FOOD_PREFERENCE_NEUTRAL, + "Clothing" = FOOD_PREFERENCE_DISLIKED, + "Nuts" = FOOD_PREFERENCE_NEUTRAL, + "Seafood" = FOOD_PREFERENCE_NEUTRAL, + "Oranges" = FOOD_PREFERENCE_NEUTRAL, + "Bugs" = FOOD_PREFERENCE_DISLIKED, + "Gore" = FOOD_PREFERENCE_TOXIC, // Toxic by default to allow default values to be usable from the start. + "Bloody" = FOOD_PREFERENCE_TOXIC, // Toxic by default to allow default values to be usable from the start. +)) + +GLOBAL_LIST_INIT(obscure_food_types, list( + "Pineapples", + "Oranges", + "Nuts" +)) diff --git a/code/__DEFINES/~skyrat_defines/food.dm b/code/__DEFINES/~skyrat_defines/food.dm new file mode 100644 index 00000000000..3454d1491e2 --- /dev/null +++ b/code/__DEFINES/~skyrat_defines/food.dm @@ -0,0 +1,12 @@ +/// Used inside the foods prefs menu. Indicates that a food is toxic. +#define FOOD_PREFERENCE_TOXIC 1 +/// Used inside the foods prefs menu. Indicates that a food is disliked. +#define FOOD_PREFERENCE_DISLIKED 2 +/// Used inside the foods prefs menu. Indicates that a food is neither liked or disliked. +#define FOOD_PREFERENCE_NEUTRAL 3 +/// Used inside the foods prefs menu. Indicates that a food is liked. +#define FOOD_PREFERENCE_LIKED 4 +/// Used inside the food prefs menu. Indicates the value of this entry is the default food like/dislike value for the food. +#define FOOD_PREFERENCE_DEFAULT 5 +/// Used inside the food prefs menu as an entry. If set to TRUE, the food is completely ignored by the verification system. Optional. +#define FOOD_PREFERENCE_OBSCURE 6 diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index f65964cd802..a242e27d21c 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -335,6 +335,10 @@ GLOBAL_LIST_EMPTY(preferences_datums) tgui.ui_interact(usr) return TRUE + if ("open_food") + GLOB.food_prefs_menu.ui_interact(usr) + return TRUE + if ("set_tricolor_preference") var/requested_preference_key = params["preference"] var/index_key = params["value"] diff --git a/code/modules/client/preferences/middleware/food.dm b/code/modules/client/preferences/middleware/food.dm new file mode 100644 index 00000000000..55e8773e8df --- /dev/null +++ b/code/modules/client/preferences/middleware/food.dm @@ -0,0 +1,156 @@ +// To make this system not a massive meta-pick for gamers, while still allowing plenty of room for unique combinations. +#define MINIMUM_REQUIRED_TOXICS 1 +#define MINIMUM_REQUIRED_DISLIKES 3 +#define MAXIMUM_LIKES 3 + +// Performance and RAM friendly menu. You can thank me later. +GLOBAL_DATUM_INIT(food_prefs_menu, /datum/food_prefs_menu, new) + +// Hahahaha, it LIVES! + +/datum/preference_middleware/food/apply_to_human(mob/living/carbon/human/target, datum/preferences/preferences, visuals_only = FALSE) + if(!length(preferences.food_preferences) || isdummy(target)) + return + var/datum/species/species_type = preferences.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + if(!species.allows_food_preferences()) + qdel(species) + return + qdel(species) + + var/fail_reason = GLOB.food_prefs_menu.is_food_invalid(preferences) + if(fail_reason) + to_chat(preferences.parent, span_announce("Your food preferences can't be set because of [fail_reason] choices! Please check your preferences!")) // Sorry, but I don't want folk sleeping on this. + return + + var/obj/item/organ/internal/tongue/target_tongue = target.get_organ_slot(ORGAN_SLOT_TONGUE) + + if(isnull(target_tongue) || !preferences.food_preferences["enabled"]) + return + + target_tongue.liked_foodtypes = NONE + target_tongue.disliked_foodtypes = NONE + target_tongue.toxic_foodtypes = NONE + + for(var/food_entry in GLOB.food_defaults) + var/list/food_default = GLOB.food_defaults[food_entry] + var/food_preference = preferences.food_preferences[food_entry] || food_default + + switch(food_preference) + if(FOOD_PREFERENCE_LIKED) + target_tongue.liked_foodtypes |= GLOB.food_ic_flag_to_bitflag[food_entry] + if(FOOD_PREFERENCE_DISLIKED) + target_tongue.disliked_foodtypes |= GLOB.food_ic_flag_to_bitflag[food_entry] + if(FOOD_PREFERENCE_TOXIC) + target_tongue.toxic_foodtypes |= GLOB.food_ic_flag_to_bitflag[food_entry] + +/// Food prefs menu datum. Global datum for performance and memory reasons. +/datum/food_prefs_menu/ui_interact(mob/dead/new_player/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "FoodPreferences", "Food Preferences") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/food_prefs_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + var/datum/preferences/preferences = ui?.user?.client?.prefs + if(!preferences) + return + + switch(action) + if("reset") + preferences.food_preferences.Cut() + preferences.food_preferences = list() + return TRUE + + if("toggle") + preferences.food_preferences["enabled"] = !preferences.food_preferences["enabled"] + return TRUE + + if("change_food") + var/food_name = params["food_name"] + var/food_preference = params["food_preference"] + + if(!food_name || !preferences || !food_preference || !(food_preference in list(FOOD_PREFERENCE_LIKED, FOOD_PREFERENCE_NEUTRAL, FOOD_PREFERENCE_DISLIKED, FOOD_PREFERENCE_TOXIC))) + return TRUE + + // Do some simple validation for max liked foods. Full validation is done on spawn and in the actual menu. + var/liked_food_length = 0 + + for(var/food_entry in preferences.food_preferences) + if(food_entry in GLOB.obscure_food_types) + continue + + if(preferences.food_preferences[food_entry] == FOOD_PREFERENCE_LIKED) + liked_food_length++ + if(liked_food_length > MAXIMUM_LIKES) + preferences.food_preferences.Remove(food_entry) + if(liked_food_length > MAXIMUM_LIKES || (food_preference == FOOD_PREFERENCE_LIKED && liked_food_length == MAXIMUM_LIKES)) // Equals as well, if we're setting a liked food! + if(food_name in GLOB.obscure_food_types) + return TRUE + + preferences.food_preferences[food_name] = food_preference + return TRUE + +/datum/preferences/ui_state(mob/user) + return GLOB.always_state + +/datum/food_prefs_menu/ui_status(mob/user, datum/ui_state/state) + return UI_INTERACTIVE // Prefs can be accessed from anywhere. + +/datum/food_prefs_menu/ui_static_data(mob/user) + return list( + "food_types" = GLOB.food_defaults, + "obscure_food_types" = GLOB.obscure_food_types, + ) + +/datum/food_prefs_menu/ui_data(mob/user) + var/datum/preferences/preferences = user.client.prefs + + var/datum/species/species_type = preferences.read_preference(/datum/preference/choiced/species) + var/datum/species/species = new species_type + + var/list/data = list( + "selection" = preferences.food_preferences, + "enabled" = preferences.food_preferences["enabled"], + "invalid" = is_food_invalid(preferences), + "race_disabled" = !species.allows_food_preferences(), + ) + qdel(species) + return data + +/// Checks the provided preferences datum to make sure the food pref values are valid. Does not check if the food preferences value is null. +/datum/food_prefs_menu/proc/is_food_invalid(datum/preferences/preferences) + var/liked_food_length = 0 + var/disliked_food_length = 0 + var/toxic_food_length = 0 + + for(var/food_entry in GLOB.food_defaults) + var/list/food_default = GLOB.food_defaults[food_entry] + var/food_preference = preferences.food_preferences[food_entry] || food_default + + if(food_entry in GLOB.obscure_food_types) + continue + + switch(food_preference) + if(FOOD_PREFERENCE_LIKED) + liked_food_length++ + if(FOOD_PREFERENCE_DISLIKED) + disliked_food_length++ + if(FOOD_PREFERENCE_TOXIC) + toxic_food_length++ + + if(liked_food_length > MAXIMUM_LIKES) + return "too many liked choices" + if(disliked_food_length + toxic_food_length < MINIMUM_REQUIRED_DISLIKES) + return "too few disliked choices" + if(toxic_food_length < MINIMUM_REQUIRED_TOXICS) + return "too few toxic choices" + +#undef MINIMUM_REQUIRED_TOXICS +#undef MINIMUM_REQUIRED_DISLIKES +#undef MAXIMUM_LIKES diff --git a/modular_skyrat/master_files/code/datums/quirks/neutral_quirks/food.dm b/modular_skyrat/master_files/code/datums/quirks/neutral_quirks/food.dm new file mode 100644 index 00000000000..b3aaa0d23b2 --- /dev/null +++ b/modular_skyrat/master_files/code/datums/quirks/neutral_quirks/food.dm @@ -0,0 +1,15 @@ +// Food preferences replaces this. +/datum/quirk/deviant_tastes + hidden_quirk = TRUE + +// Food preferences replaces this. +/datum/quirk/pineapple_hater + hidden_quirk = TRUE + +// Food preferences replaces this. +/datum/quirk/pineapple_liker + hidden_quirk = TRUE + +// You can become a vegan with food preferences +/datum/quirk/vegetarian + hidden_quirk = TRUE diff --git a/modular_skyrat/master_files/code/modules/client/preferences.dm b/modular_skyrat/master_files/code/modules/client/preferences.dm index 52a70bd907c..52a22840192 100644 --- a/modular_skyrat/master_files/code/modules/client/preferences.dm +++ b/modular_skyrat/master_files/code/modules/client/preferences.dm @@ -53,6 +53,10 @@ /// A photo of the character, visible on close examine var/headshot = "" +/// An assoc list of food types to liked or dislike values. If null or empty, default species tastes are used instead on application. + /// If a food doesn't exist in this list, it uses the default value. + var/list/food_preferences = list() + /datum/preferences/proc/species_updated(species_type) all_quirks = list() // Reset cultural stuff diff --git a/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm index be228d89f10..791bfcd39bd 100644 --- a/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm +++ b/modular_skyrat/master_files/code/modules/client/preferences_savefile.dm @@ -96,6 +96,8 @@ headshot = save_data["headshot"] + food_preferences = SANITIZE_LIST(save_data["food_preferences"]) + if(needs_update >= 0) update_character_skyrat(needs_update, save_data) // needs_update == savefile_version if we need an update (positive integer) @@ -273,6 +275,7 @@ save_data["languages"] = languages save_data["headshot"] = headshot save_data["modular_version"] = MODULAR_SAVEFILE_VERSION_MAX + save_data["food_preferences"] = food_preferences /datum/preferences/proc/update_mutant_bodyparts(datum/preference/preference) diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm index 1bb68a1f10f..ac68a44e2fd 100644 --- a/modular_skyrat/master_files/code/modules/mob/living/human/species.dm +++ b/modular_skyrat/master_files/code/modules/mob/living/human/species.dm @@ -1,3 +1,6 @@ +/datum/species/proc/allows_food_preferences() + return TRUE + /** * Returns a list of strings representing features this species has. * diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species_types/ethereal.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/ethereal.dm new file mode 100644 index 00000000000..54d247f27a2 --- /dev/null +++ b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/ethereal.dm @@ -0,0 +1,2 @@ +/datum/species/ethereal/allows_food_preferences() + return FALSE diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species_types/flypeople.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/flypeople.dm new file mode 100644 index 00000000000..1f18607844f --- /dev/null +++ b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/flypeople.dm @@ -0,0 +1,2 @@ +/datum/species/fly/allows_food_preferences() + return FALSE diff --git a/modular_skyrat/master_files/code/modules/mob/living/human/species_types/plasmamen.dm b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/plasmamen.dm new file mode 100644 index 00000000000..f41919c49c0 --- /dev/null +++ b/modular_skyrat/master_files/code/modules/mob/living/human/species_types/plasmamen.dm @@ -0,0 +1,2 @@ +/datum/species/plasmaman/allows_food_preferences() + return FALSE diff --git a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage/hemophage_species.dm b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage/hemophage_species.dm index 40c63804168..9c4d89d918e 100644 --- a/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage/hemophage_species.dm +++ b/modular_skyrat/modules/customization/modules/mob/living/carbon/human/species/hemophage/hemophage_species.dm @@ -28,6 +28,9 @@ skinned_type = /obj/item/stack/sheet/animalhide/human veteran_only = TRUE +/datum/species/hemophage/allows_food_preferences() + return FALSE + /datum/species/hemophage/get_default_mutant_bodyparts() return list( "legs" = list("Normal Legs", FALSE), diff --git a/modular_skyrat/modules/synths/code/species/synthetic.dm b/modular_skyrat/modules/synths/code/species/synthetic.dm index 90a01e09ed9..4d035c7dfad 100644 --- a/modular_skyrat/modules/synths/code/species/synthetic.dm +++ b/modular_skyrat/modules/synths/code/species/synthetic.dm @@ -51,6 +51,9 @@ /// This is the screen that is given to the user after they get revived. On death, their screen is temporarily set to BSOD before it turns off, hence the need for this var. var/saved_screen = "Blank" +/datum/species/synthetic/allows_food_preferences() + return FALSE + /datum/species/synthetic/get_default_mutant_bodyparts() return list( "tail" = list("None", FALSE), diff --git a/tgstation.dme b/tgstation.dme index 69ec30eb9be..fad9b2ac409 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -416,6 +416,7 @@ #include "code\__DEFINES\~skyrat_defines\faction.dm" #include "code\__DEFINES\~skyrat_defines\factions.dm" #include "code\__DEFINES\~skyrat_defines\flavor_defines.dm" +#include "code\__DEFINES\~skyrat_defines\food.dm" #include "code\__DEFINES\~skyrat_defines\game_options.dm" #include "code\__DEFINES\~skyrat_defines\hud.dm" #include "code\__DEFINES\~skyrat_defines\id_cards.dm" @@ -466,6 +467,7 @@ #include "code\__DEFINES\~skyrat_defines\vv.dm" #include "code\__DEFINES\~skyrat_defines\wounds.dm" #include "code\__DEFINES\~skyrat_defines\_globalvars\bitfields.dm" +#include "code\__DEFINES\~skyrat_defines\_globalvars\food.dm" #include "code\__DEFINES\~skyrat_defines\_globalvars\logging.dm" #include "code\__DEFINES\~skyrat_defines\_globalvars\lists\mapping.dm" #include "code\__DEFINES\~skyrat_defines\_HELPERS\atmos_mapping_helpers.dm" @@ -3757,6 +3759,7 @@ #include "code\modules\client\preferences\window_flashing.dm" #include "code\modules\client\preferences\middleware\_middleware.dm" #include "code\modules\client\preferences\middleware\antags.dm" +#include "code\modules\client\preferences\middleware\food.dm" #include "code\modules\client\preferences\middleware\jobs.dm" #include "code\modules\client\preferences\middleware\keybindings.dm" #include "code\modules\client\preferences\middleware\legacy_toggles.dm" @@ -6284,6 +6287,7 @@ #include "modular_skyrat\master_files\code\datums\quirks\negative_quirks\narcolepsy.dm" #include "modular_skyrat\master_files\code\datums\quirks\negative_quirks\nerve_staple.dm" #include "modular_skyrat\master_files\code\datums\quirks\neutral_quirks\equipping.dm" +#include "modular_skyrat\master_files\code\datums\quirks\neutral_quirks\food.dm" #include "modular_skyrat\master_files\code\datums\quirks\neutral_quirks\lungs.dm" #include "modular_skyrat\master_files\code\datums\quirks\positive_quirks\life_savings.dm" #include "modular_skyrat\master_files\code\datums\records\record.dm" @@ -6524,6 +6528,9 @@ #include "modular_skyrat\master_files\code\modules\mob\living\carbon\human\species_type\snail.dm" #include "modular_skyrat\master_files\code\modules\mob\living\human\monkey.dm" #include "modular_skyrat\master_files\code\modules\mob\living\human\species.dm" +#include "modular_skyrat\master_files\code\modules\mob\living\human\species_types\ethereal.dm" +#include "modular_skyrat\master_files\code\modules\mob\living\human\species_types\flypeople.dm" +#include "modular_skyrat\master_files\code\modules\mob\living\human\species_types\plasmamen.dm" #include "modular_skyrat\master_files\code\modules\mob\living\sillicon\robot.dm" #include "modular_skyrat\master_files\code\modules\mob\living\simple_animal\simple_animal.dm" #include "modular_skyrat\master_files\code\modules\mob\living\simple_animal\friendly\bumbles.dm" diff --git a/tgui/packages/tgui/interfaces/FoodPreferences.tsx b/tgui/packages/tgui/interfaces/FoodPreferences.tsx new file mode 100644 index 00000000000..39c94d2e23c --- /dev/null +++ b/tgui/packages/tgui/interfaces/FoodPreferences.tsx @@ -0,0 +1,224 @@ +// THIS IS A SKYRAT UI FILE +import { useBackend } from '../backend'; +import { + Box, + Dimmer, + Divider, + Icon, + Section, + Stack, + StyleableSection, + Tooltip, +} from '../components'; +import { Button } from '../components/Button'; +import { Window } from '../layouts'; + +type Data = { + food_types: Record; + obscure_food_types: string; + selection: Record; + enabled: boolean; + invalid: string; + race_disabled: boolean; +}; + +const FOOD_TOXIC = 1; +const FOOD_DISLIKED = 2; +const FOOD_NEUTRAL = 3; +const FOOD_LIKED = 4; +const DEFAULT_FOOD_VALUE = 4; +const OBSCURE_FOOD = 5; + +export const FoodPreferences = (props) => { + const { act, data } = useBackend(); + + return ( + + + { + + + + + + + + } + > + {(data.race_disabled && ( + + You're using a race which isn't affected by food + preferences! + + )) || + (!data.enabled && ( + Your food preferences are disabled! + ))} + + {Object.entries(data.food_types).map((element) => { + const { 0: foodName, 1: foodPointValues } = element; + return ( + +
+ {foodName} + {data.obscure_food_types[foodName] && ( + + + + + + )} + + } + > + Toxic} + color="olive" + tooltip="Your character will almost immediately throw up on eating anything toxic." + /> + Disliked} + color="red" + tooltip="Your character will become grossed out, before eventually throwing up after a decent intake of disliked food." + /> + Neutral} + color="yellow" + tooltip="Your character has very little to say about something that's neutral." + /> + Liked} + color="green" + tooltip="Your character will enjoy anything that's liked." + /> +
+
+ ); + })} +
+
+ } +
+
+ ); +}; + +const FoodButton = (props) => { + const { act } = useBackend(); + const { foodName, foodPreference, color, selected, ...rest } = props; + return ( +