diff --git a/code/__DEFINES/crafting.dm b/code/__DEFINES/crafting.dm
index 09b35c5d093..5d2cc6632e9 100644
--- a/code/__DEFINES/crafting.dm
+++ b/code/__DEFINES/crafting.dm
@@ -27,9 +27,9 @@
#define CAT_SPAGHETTI "Spaghettis"
#define CAT_ICE "Frozen"
-#define RECIPE_MICROWAVE "Microwave"
-#define RECIPE_OVEN "Oven"
-#define RECIPE_GRILL "Grill"
-#define RECIPE_CANDY "Candy"
+#define RECIPE_MICROWAVE "microwave"
+#define RECIPE_OVEN "oven"
+#define RECIPE_GRILL "grill"
+#define RECIPE_CANDY "candy machine"
#define RECIPE_FAIL null
diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm
index a837dc7a72b..790c9f9ea66 100644
--- a/code/__DEFINES/misc_defines.dm
+++ b/code/__DEFINES/misc_defines.dm
@@ -703,3 +703,7 @@ do { \
/// A helper used by `restrict_file_types.py` to identify types to restrict in a file. Not used by byond at all.
#define RESTRICT_TYPE(type) // do nothing
+
+#define INGREDIENT_CHECK_EXACT 1
+#define INGREDIENT_CHECK_FAILURE 0
+#define INGREDIENT_CHECK_SURPLUS -1
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index 3cb5928179a..e3778abe37f 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -233,6 +233,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_TABLE_LEAP "table_leap" // Lets bartender and chef mount tables faster
#define TRAIT_NEVER_MISSES_DISPOSALS "trait_never_misses_disposals" // For janitors landing disposal throws
#define TRAIT_SLEIGHT_OF_HAND "sleight_of_hand"
+#define TRAIT_KNOWS_COOKING_RECIPES "knows_cooking_recipes"
//***** ITEM AND MOB TRAITS *****//
/// Show what machine/door wires do when held.
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 79d3208719c..ca7ef0c3adc 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -2082,3 +2082,18 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
/proc/is_color_dark(color, threshold = 25)
var/hsl = rgb2num(color, COLORSPACE_HSL)
return hsl[3] < threshold
+
+/**
+ * This proc takes a list of types, and returns them in the format below.
+ * [type] = amount of type in list.
+ * Useful for recipes.
+ */
+/proc/type_list_to_counted_assoc_list(list/origin_list)
+ var/list/return_list = list()
+ for(var/datum/path as anything in origin_list)
+ if(isdatum(path))
+ path = path.type
+ if(!return_list[path])
+ return_list[path] = 0
+ return_list[path] += 1
+ return return_list
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 2e1fc13f5db..3d3cf117030 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -81,7 +81,6 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NOSELFIGNITION_HEAD_ONLY" = TRAIT_NOSELFIGNITION_HEAD_ONLY,
"TRAIT_CONTORTED_BODY" = TRAIT_CONTORTED_BODY,
"TRAIT_DEFLECTS_PROJECTILES" = TRAIT_DEFLECTS_PROJECTILES,
- "TRAIT_TABLE_LEAP" = TRAIT_TABLE_LEAP,
"TRAIT_DODGE_ALL_THROWN_OBJECTS" = TRAIT_DODGE_ALL_OBJECTS,
"TRAIT_SUPERMATTER_IMMUNE" = TRAIT_SUPERMATTER_IMMUNE,
"TRAIT_BADASS" = TRAIT_BADASS,
@@ -98,7 +97,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_HOLY" = TRAIT_HOLY,
"TRAIT_SLEIGHT_OF_HAND" = TRAIT_SLEIGHT_OF_HAND,
"TRAIT_TABLE_LEAP" = TRAIT_TABLE_LEAP,
- "TRAIT_NEVER_MISSES_DISPOSALS" = TRAIT_NEVER_MISSES_DISPOSALS
+ "TRAIT_NEVER_MISSES_DISPOSALS" = TRAIT_NEVER_MISSES_DISPOSALS,
+ "TRAIT_KNOWS_COOKING_RECIPES" = TRAIT_KNOWS_COOKING_RECIPES
),
/obj = list(
diff --git a/code/datums/recipe.dm b/code/datums/recipe.dm
index 8d0f7a7c0a1..f10e70ed027 100644
--- a/code/datums/recipe.dm
+++ b/code/datums/recipe.dm
@@ -42,26 +42,49 @@
var/duplicate = TRUE
var/byproduct // example: = /obj/item/kitchen/mould // byproduct to return, such as a mould or trash
-/datum/recipe/proc/check_reagents(datum/reagents/avail_reagents) //1=precisely, 0=insufficiently, -1=superfluous
- . = 1
+/datum/recipe/proc/check_reagents(datum/reagents/avail_reagents)
+ . = INGREDIENT_CHECK_EXACT
for(var/r_r in reagents)
var/aval_r_amnt = avail_reagents.get_reagent_amount(r_r)
if(!(abs(aval_r_amnt - reagents[r_r])<0.5)) //if NOT equals
if(aval_r_amnt>reagents[r_r])
- . = -1
+ . = INGREDIENT_CHECK_SURPLUS
else
- return 0
- if((reagents?(length(reagents)):(0)) < length(avail_reagents.reagent_list))
- return -1
+ return INGREDIENT_CHECK_FAILURE
+ if((reagents ? length(reagents) : 0) < length(avail_reagents.reagent_list))
+ return INGREDIENT_CHECK_SURPLUS
+
+/**
+ * Similarly to the function above, this checks for reagents, except instead of being passed a reagent holder, we're passed
+ * [reagent_id] = amount as num.
+ * Returns INGREDIENT_CHECK_EXACT if we have the precise amount thats requested.
+ * Returns INGREDIENT_CHECK_FAILURE if we do not have enough.
+ * Returns INGREDIENT_CHECK_SURPLUS if we have MORE than requested.
+ */
+/datum/recipe/proc/check_reagents_assoc_list(list/avail_reagents)
+ . = INGREDIENT_CHECK_EXACT
+ for(var/required_reagent_id in reagents)
+ var/provided_reagent_amount = avail_reagents[required_reagent_id]
+ if(!provided_reagent_amount)
+ return INGREDIENT_CHECK_FAILURE
+
+ if(abs(provided_reagent_amount - reagents[required_reagent_id]) >= 0.5) // If amount is outside of the 0.5 unit tolerance
+ if(provided_reagent_amount > reagents[required_reagent_id]) // we have more than necessary
+ . = INGREDIENT_CHECK_SURPLUS
+ else
+ return INGREDIENT_CHECK_FAILURE // we don't have enough
+
+ if(length(reagents) < length(avail_reagents))
+ return INGREDIENT_CHECK_SURPLUS
/datum/recipe/proc/check_items(obj/container, list/ignored_items = null) //1=precisely, 0=insufficiently, -1=superfluous
- . = 1
+ . = INGREDIENT_CHECK_EXACT
var/list/checklist = items ? items.Copy() : list()
for(var/obj/O in container)
if(ignored_items && is_type_in_list(O, ignored_items)) //skip if this is something we are ignoring
continue
if(!items)
- return -1
+ return INGREDIENT_CHECK_SURPLUS
var/found = 0
for(var/type in checklist)
if(istype(O,type))
@@ -69,9 +92,32 @@
found = 1
break
if(!found)
- . = -1
+ . = INGREDIENT_CHECK_SURPLUS
if(length(checklist))
- return 0
+ return INGREDIENT_CHECK_FAILURE
+
+/**
+ * Similarly to the function above, this checks for items, except instead of being passed a reagent holder, we're passed
+ * [type_path] = amount as num.
+ * Returns INGREDIENT_CHECK_EXACT if we have the precise amount thats requested.
+ * Returns INGREDIENT_CHECK_FAILURE if we do not have enough.
+ * Returns INGREDIENT_CHECK_SURPLUS if we have MORE than requested.
+ */
+/datum/recipe/proc/check_items_assoc_list(list/given_objects)
+ . = INGREDIENT_CHECK_EXACT
+ var/list/checklist = items ? items.Copy() : list()
+ for(var/obj/item/I as anything in given_objects) // path
+ var/amount_given = given_objects[I]
+ if(!length(checklist))
+ return INGREDIENT_CHECK_SURPLUS
+
+ for(var/i in 1 to amount_given)
+ if(I in checklist)
+ checklist -= I
+ else if(I in items) // make sure the recipe requires this item before declaring it surplus
+ . = INGREDIENT_CHECK_SURPLUS
+ if(length(checklist)) // we didnt get everything
+ return INGREDIENT_CHECK_FAILURE
//general version
/datum/recipe/proc/make(obj/container)
@@ -94,7 +140,7 @@
container.reagents.clear_reagents()
return result_obj
-/proc/select_recipe(list/datum/recipe/available_recipes, obj/obj, exact = 1 as num, list/ignored_items = null)
+/proc/select_recipe(list/datum/recipe/available_recipes, obj/obj, exact = INGREDIENT_CHECK_EXACT, list/ignored_items = null)
if(!exact)
exact = -1
var/list/datum/recipe/possible_recipes = new
diff --git a/code/datums/spells/chef.dm b/code/datums/spells/chef.dm
new file mode 100644
index 00000000000..642f5476f44
--- /dev/null
+++ b/code/datums/spells/chef.dm
@@ -0,0 +1,17 @@
+/datum/spell/chef/expert_chef
+ name = "Expert Chef Knowledge"
+ desc = "Find things you can cook with the items in reach."
+ school = "chef"
+ clothes_req = FALSE
+ base_cooldown = 5 SECONDS
+ human_req = TRUE
+ action_icon_state = "chef"
+ action_background_icon_state = "bg_default"
+ still_recharging_msg = "All this thinking makes your head hurt, wait a bit longer."
+
+/datum/spell/chef/expert_chef/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/H = targets[1]
+ H.expert_chef_knowledge()
+
+/datum/spell/chef/expert_chef/create_new_targeting()
+ return new /datum/spell_targeting/self
diff --git a/code/game/jobs/job/support.dm b/code/game/jobs/job/support.dm
index c8678be4e11..b24597ec1fa 100644
--- a/code/game/jobs/job/support.dm
+++ b/code/game/jobs/job/support.dm
@@ -244,7 +244,9 @@
var/datum/martial_art/cqc/under_siege/justacook = new
justacook.teach(H) // requires mind
ADD_TRAIT(H.mind, TRAIT_TABLE_LEAP, ROUNDSTART_TRAIT)
-
+ ADD_TRAIT(H.mind, TRAIT_KNOWS_COOKING_RECIPES, ROUNDSTART_TRAIT)
+ if(H.mind)
+ H.mind.AddSpell(new /datum/spell/chef/expert_chef)
/datum/job/hydro
title = "Botanist"
diff --git a/code/game/objects/items/trash.dm b/code/game/objects/items/trash.dm
index e3e6b5d0b43..f9a3c96d3dd 100644
--- a/code/game/objects/items/trash.dm
+++ b/code/game/objects/items/trash.dm
@@ -38,8 +38,9 @@
icon_state = "twimsts"
/obj/item/trash/popcorn
- name = "Popcorn"
+ name = "popcorn"
icon_state = "popcorn"
+ gender = PLURAL
/obj/item/trash/sosjerky
name = "Scaredy's Private Reserve Beef Jerky"
diff --git a/code/modules/food_and_drinks/food/foods/misc_food.dm b/code/modules/food_and_drinks/food/foods/misc_food.dm
index 7d5b6bcf6b0..ae9e946f60e 100644
--- a/code/modules/food_and_drinks/food/foods/misc_food.dm
+++ b/code/modules/food_and_drinks/food/foods/misc_food.dm
@@ -283,6 +283,7 @@
name = "popcorn"
desc = "Now let's find some cinema."
icon_state = "popcorn"
+ gender = PLURAL
trash = /obj/item/trash/popcorn
var/unpopped = 0
filling_color = "#FFFAD4"
diff --git a/code/modules/food_and_drinks/kitchen_machinery/kitchen_machine.dm b/code/modules/food_and_drinks/kitchen_machinery/kitchen_machine.dm
index 2058df28982..09e60e96ff3 100644
--- a/code/modules/food_and_drinks/kitchen_machinery/kitchen_machine.dm
+++ b/code/modules/food_and_drinks/kitchen_machinery/kitchen_machine.dm
@@ -440,6 +440,19 @@
if(dat)
. += "It contains:
[dat]"
+ if(!HAS_MIND_TRAIT(user, TRAIT_KNOWS_COOKING_RECIPES))
+ return
+
+ if(locate(/obj/item/mixing_bowl) in contents)
+ . += "You can't see inside the mixing bowl, you're not sure what it would do."
+ return
+
+ var/list/recipes = choose_recipes()
+ if(length(recipes) == 1 && recipes[1][2] != RECIPE_FAIL)
+ var/datum/recipe/recipe = recipes[1][2]
+ var/obj/item/result = recipe.result
+ . += "Your expert chef knowledge tells you that this would make \a [initial(result.name)]."
+
/obj/machinery/kitchen_machine/attack_hand(mob/user)
if(stat & (BROKEN|NOPOWER) || panel_open || !anchored)
return
diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm
index eade62cabf0..dc78288d967 100644
--- a/code/modules/mob/living/carbon/human/human_mob.dm
+++ b/code/modules/mob/living/carbon/human/human_mob.dm
@@ -1967,3 +1967,67 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
set category = "IC"
update_flavor_text()
+
+/mob/living/carbon/human/proc/expert_chef_knowledge()
+ if(!HAS_TRAIT(mind, TRAIT_KNOWS_COOKING_RECIPES))
+ return
+
+ var/list/possible_cookware = view(1, src)
+
+ for(var/obj/item/storage/storage in possible_cookware)
+ possible_cookware += storage.contents
+ possible_cookware -= storage
+
+ shuffle_inplace(possible_cookware)
+
+ var/list/available_ingredients = list()
+ var/list/available_reagents = list()
+ for(var/obj/item/item in possible_cookware)
+ if(item.container_type == OPENCONTAINER && item.reagents)
+ for(var/datum/reagent/reagent in item.reagents.reagent_list)
+ if(!available_reagents[reagent.id])
+ available_reagents[reagent.id] = 0
+ available_reagents[reagent.id] += reagent.volume
+ else
+ if(!available_ingredients[item.type])
+ available_ingredients[item.type] = 0
+ available_ingredients[item.type] += 1
+
+ var/list/possible_recipes = list()
+ for(var/datum/recipe/recipe_type as anything in subtypesof(/datum/recipe))
+ if(!recipe_type.result)
+ continue
+ var/datum/recipe/recipe = new recipe_type()
+ if(recipe.check_reagents_assoc_list(available_reagents) != INGREDIENT_CHECK_FAILURE && recipe.check_items_assoc_list(available_ingredients) != INGREDIENT_CHECK_FAILURE)
+ if(istype(recipe, /datum/recipe/microwave))
+ possible_recipes[recipe] = RECIPE_MICROWAVE
+ else if(istype(recipe, /datum/recipe/oven))
+ possible_recipes[recipe] = RECIPE_OVEN
+ else if(istype(recipe, /datum/recipe/grill))
+ possible_recipes[recipe] = RECIPE_GRILL
+ else if(istype(recipe, /datum/recipe/candy))
+ possible_recipes[recipe] = RECIPE_CANDY
+ else
+ possible_recipes[recipe] = "something? This shouldn't happen, make a bug report"
+
+ if(!length(possible_recipes))
+ to_chat(src, "You can't think of anything to cook with the items around you.")
+ return
+
+ var/list/message = list("You draw upon your extensive experience in space food, contemplating what you could make with the items around you...")
+ for(var/datum/recipe/recipe in possible_recipes)
+ var/list/assoc = type_list_to_counted_assoc_list(recipe.items)
+ var/list/ingredient_list = list()
+ for(var/obj/item/path_key as anything in assoc)
+ ingredient_list += "[assoc[path_key]] [initial(path_key.name)]\s"
+
+ var/list/required_reagents = list()
+ for(var/reagent_id in recipe.reagents)
+ var/datum/reagent/temp = GLOB.chemical_reagents_list[reagent_id]
+ if(temp)
+ required_reagents += "[recipe.reagents[reagent_id]] unit\s of [temp.name]"
+
+ var/obj/item/result = recipe.result
+ message += "\tI could make [result.gender == PLURAL ? "some" : "a"] [bicon(result)] [result.name] by using \a [possible_recipes[recipe]] with [english_list(ingredient_list)][length(required_reagents) ? ", along with [english_list(required_reagents)]" : ""]."
+ qdel(recipe)
+ to_chat(src, chat_box_examine(message.Join("
")))
diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi
index e9b7ef94895..ed1b9f9f312 100644
Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 8f0b4deab90..e307ba51e9b 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -555,6 +555,7 @@
#include "code\datums\spells\charge.dm"
#include "code\datums\spells\charge_up.dm"
#include "code\datums\spells\charge_up_bounce.dm"
+#include "code\datums\spells\chef.dm"
#include "code\datums\spells\cluwne.dm"
#include "code\datums\spells\conjure.dm"
#include "code\datums\spells\conjure_item.dm"