mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 16:05:07 +00:00
My original plan was to just implement materials into crafting so that items would inherit the materials of their components, allowing for some interesting stuff if the material flags of the item allow it. However to my dismay crafting is a pile of old tech debt, starting from the old `del_reqs` and `CheckParts` which still contain lines about old janky bandaids that are no longer in use nor reachable, up to the `customizable_reagent_holder` component which has some harddel issues when your custom food is sliced, and items used in food recipes not being deleted and instead stored inside the result with no purpose as well as other inconsistencies like stack recipes that transfer materials having counterparts in the UI that don't do that. EDIT: Several things have come up while working on this, so I apologise that it ended up changing over 100+ files. I managed to atomize some of the changes, but it's a bit tedious. EDIT: TLDR because I was told this section is too vague and there's too much going on. This PR: - Improves the dated crafting code (not the UI). - replaced `atom/CheckParts` and `crafting_recipe/on_craft_completion` with `atom/on_craft_completion`. - Reqs used in food recipes are now deleted by default and not stored inside the result (they did nothing). - Renames the customizable_reagent_holder comp and improves it (No harddels/ref issues). - Adds a unit test that tries to craft all recipes to see what's wrong (it skips some of the much more specific reqs for now). - In the unit test is also the code to make sure materials of the crafted item and a non-crafted item of the same type are roughly the same, so far only applied to food. - Some mild material/food refactoring around the fact that food item code has been changed to support materials. Improving the backbone of the crafting system. Also materials and food code. 🆑 refactor: Refactored crafting backend. Report possible pesky bugs. balance: the MEAT backpack (from the MEAT cargo pack) may be a smidge different because of code standardization. /🆑
763 lines
30 KiB
Plaintext
763 lines
30 KiB
Plaintext
/*!
|
|
|
|
This component makes it possible to make things edible. What this means is that you can take a bite or force someone to take a bite (in the case of items).
|
|
These items take a specific time to eat, and can do most of the things our original food items could.
|
|
|
|
Behavior that's still missing from this component that original food items had that should either be put into separate components or somewhere else:
|
|
Components:
|
|
Drying component (jerky etc)
|
|
Processable component (Slicing and cooking behavior essentialy, making it go from item A to B when conditions are met.)
|
|
|
|
Misc:
|
|
Something for cakes (You can store things inside)
|
|
*/
|
|
|
|
#define DEFAULT_EDIBLE_VOLUME 50
|
|
|
|
/datum/component/edible
|
|
dupe_mode = COMPONENT_DUPE_SOURCES
|
|
///Amount of reagents taken per bite
|
|
var/bite_consumption = 2
|
|
///Amount of bites taken so far
|
|
var/bitecount = 0
|
|
///Flags for food
|
|
var/food_flags = NONE
|
|
///Bitfield of the types of this food
|
|
var/foodtypes = NONE
|
|
///Amount of seconds it takes to eat this food
|
|
var/eat_time = 3 SECONDS
|
|
///Defines how much it lowers someones satiety (Need to eat, essentialy)
|
|
var/junkiness = 0
|
|
///Message to send when eating
|
|
var/list/eatverbs
|
|
///Callback to be ran for when you take a bite of something
|
|
var/datum/callback/after_eat
|
|
///Callback to be ran for when you finish eating something
|
|
var/datum/callback/on_consume
|
|
///Callback to be ran for when the code check if the food is liked, allowing for unique overrides for special foods like donuts with cops.
|
|
var/datum/callback/check_liked
|
|
///Last time we checked for food likes
|
|
var/last_check_time
|
|
///Assoc list of sources and their foodtypes
|
|
var/list/foodtypes_by_source = list()
|
|
///Assoc list of sources and their food flags
|
|
var/list/food_flags_by_source = list()
|
|
///Assoc list of sources and their junkiness
|
|
var/list/junkiness_by_source = list()
|
|
|
|
/datum/component/edible/Initialize(
|
|
list/initial_reagents,
|
|
food_flags = NONE,
|
|
foodtypes = NONE,
|
|
volume = DEFAULT_EDIBLE_VOLUME,
|
|
eat_time = 1 SECONDS,
|
|
list/tastes,
|
|
list/eatverbs = list("bite", "chew", "nibble", "gnaw", "gobble", "chomp"),
|
|
bite_consumption = 2,
|
|
junkiness,
|
|
datum/callback/after_eat,
|
|
datum/callback/on_consume,
|
|
datum/callback/check_liked,
|
|
reagent_purity = 0.5,
|
|
)
|
|
if(!isatom(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
// If these args are not explicitly stated when initializing the component
|
|
// Use the defaults provided in this proc definition, so we don't have to worry
|
|
// about these being null. We cannot rely on on_add_source() for this lest these
|
|
// end up being unwantedly overriden by other sources.
|
|
src.bite_consumption = bite_consumption
|
|
src.food_flags = food_flags
|
|
src.foodtypes = foodtypes
|
|
src.eat_time = eat_time
|
|
src.eatverbs = string_list(eatverbs)
|
|
|
|
/datum/component/edible/RegisterWithParent()
|
|
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(examine))
|
|
RegisterSignal(parent, COMSIG_ATOM_ATTACK_ANIMAL, PROC_REF(UseByAnimal))
|
|
RegisterSignal(parent, COMSIG_ATOM_ON_CRAFT, PROC_REF(OnCraft))
|
|
RegisterSignal(parent, COMSIG_OOZE_EAT_ATOM, PROC_REF(on_ooze_eat))
|
|
RegisterSignal(parent, COMSIG_FOOD_INGREDIENT_ADDED, PROC_REF(edible_ingredient_added))
|
|
RegisterSignal(parent, COMSIG_ATOM_CREATEDBY_PROCESSING, PROC_REF(created_by_processing))
|
|
RegisterSignal(parent, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, PROC_REF(on_material_effects))
|
|
RegisterSignal(parent, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS, PROC_REF(on_remove_material_effects))
|
|
|
|
if(isturf(parent))
|
|
RegisterSignal(parent, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
|
|
else
|
|
var/static/list/loc_connections = list(COMSIG_ATOM_ENTERED = PROC_REF(on_entered))
|
|
AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)
|
|
|
|
if(isitem(parent))
|
|
RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(UseFromHand))
|
|
RegisterSignal(parent, COMSIG_ITEM_USED_AS_INGREDIENT, PROC_REF(used_to_customize))
|
|
|
|
var/obj/item/item = parent
|
|
if(!item.grind_results)
|
|
item.grind_results = list() //If this doesn't already exist, add it as an empty list. This is needed for the grinder to accept it.
|
|
|
|
else if(isturf(parent) || isstructure(parent))
|
|
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(TryToEatIt))
|
|
|
|
if(foodtypes & GORE)
|
|
ADD_TRAIT(parent, TRAIT_VALID_DNA_INFUSION, REF(src))
|
|
|
|
/datum/component/edible/UnregisterFromParent()
|
|
UnregisterSignal(parent, list(
|
|
COMSIG_ATOM_ATTACK_ANIMAL,
|
|
COMSIG_ATOM_ATTACK_HAND,
|
|
COMSIG_ATOM_ON_CRAFT,
|
|
COMSIG_ATOM_CREATEDBY_PROCESSING,
|
|
COMSIG_ATOM_ENTERED,
|
|
COMSIG_FOOD_INGREDIENT_ADDED,
|
|
COMSIG_ITEM_ATTACK,
|
|
COMSIG_ITEM_USED_AS_INGREDIENT,
|
|
COMSIG_OOZE_EAT_ATOM,
|
|
COMSIG_ATOM_EXAMINE,
|
|
))
|
|
|
|
qdel(GetComponent(/datum/component/connect_loc_behalf))
|
|
|
|
if(foodtypes & GORE)
|
|
REMOVE_TRAIT(parent, TRAIT_VALID_DNA_INFUSION, REF(src))
|
|
|
|
/datum/component/edible/allow_source_update(source)
|
|
return source == SOURCE_EDIBLE_INNATE
|
|
|
|
/datum/component/edible/on_source_add(
|
|
source,
|
|
list/initial_reagents,
|
|
food_flags,
|
|
foodtypes,
|
|
volume,
|
|
eat_time,
|
|
list/tastes,
|
|
list/eatverbs,
|
|
bite_consumption,
|
|
junkiness,
|
|
datum/callback/after_eat,
|
|
datum/callback/on_consume,
|
|
datum/callback/check_liked,
|
|
reagent_purity = 0.5,
|
|
)
|
|
. = ..()
|
|
|
|
var/recalculate = FALSE
|
|
if(!isnull(foodtypes))
|
|
if(foodtypes_by_source[source]) //foodtypes being overriden
|
|
recalculate = TRUE
|
|
foodtypes_by_source[source] = foodtypes
|
|
if(!isnull(food_flags))
|
|
if(food_flags_by_source[source]) //food_flags being overriden
|
|
recalculate = TRUE
|
|
food_flags_by_source[source] = food_flags
|
|
if(!isnull(junkiness))
|
|
src.junkiness += junkiness - junkiness_by_source[source]
|
|
junkiness_by_source[source] = junkiness
|
|
|
|
if(recalculate)
|
|
recalculate_food_flags()
|
|
else
|
|
// nothing is being removed
|
|
src.food_flags |= food_flags
|
|
src.foodtypes |= foodtypes
|
|
if(foodtypes & GORE)
|
|
ADD_TRAIT(parent, TRAIT_VALID_DNA_INFUSION, REF(src))
|
|
|
|
// add newly passed in reagents
|
|
setup_initial_reagents(initial_reagents, reagent_purity, tastes, volume)
|
|
|
|
//Only the innate source is allowed to change the following vars if there are a plurality of sources.
|
|
if(source != SOURCE_EDIBLE_INNATE && length(sources) > 1)
|
|
return
|
|
|
|
// add all new eatverbs to the list
|
|
if(islist(eatverbs))
|
|
var/list/cached_verbs = src.eatverbs
|
|
if(islist(cached_verbs))
|
|
// eatverbs becomes a combination of existing verbs and new ones
|
|
src.eatverbs = string_list(cached_verbs | eatverbs)
|
|
else
|
|
src.eatverbs = string_list(eatverbs)
|
|
// just set these directly
|
|
if(!isnull(bite_consumption))
|
|
src.bite_consumption = bite_consumption
|
|
if(!isnull(eat_time))
|
|
src.eat_time = eat_time
|
|
if(!isnull(after_eat))
|
|
src.after_eat = after_eat
|
|
if(!isnull(on_consume))
|
|
src.on_consume = on_consume
|
|
if(!isnull(check_liked))
|
|
src.check_liked = check_liked
|
|
|
|
/datum/component/edible/on_source_remove(source)
|
|
//rebuild the foodtypes and food_flags bitfields without the removed source
|
|
foodtypes_by_source -= source
|
|
food_flags_by_source -= source
|
|
junkiness -= junkiness_by_source[source]
|
|
junkiness_by_source -= source
|
|
recalculate_food_flags()
|
|
return ..()
|
|
|
|
/datum/component/edible/proc/recalculate_food_flags()
|
|
foodtypes = NONE
|
|
food_flags = NONE
|
|
for(var/source_key in foodtypes_by_source)
|
|
foodtypes |= foodtypes_by_source[source_key]
|
|
food_flags |= food_flags_by_source[source_key]
|
|
if(foodtypes & GORE)
|
|
ADD_TRAIT(parent, TRAIT_VALID_DNA_INFUSION, REF(src))
|
|
else
|
|
REMOVE_TRAIT(parent, TRAIT_VALID_DNA_INFUSION, REF(src))
|
|
|
|
/datum/component/edible/Destroy(force)
|
|
after_eat = null
|
|
on_consume = null
|
|
check_liked = null
|
|
return ..()
|
|
|
|
/// Sets up the initial reagents of the food.
|
|
/datum/component/edible/proc/setup_initial_reagents(list/reagents, reagent_purity, list/tastes, volume)
|
|
var/atom/owner = parent
|
|
if(!owner.reagents)
|
|
owner.create_reagents(volume || DEFAULT_EDIBLE_VOLUME, INJECTABLE)
|
|
else if(volume > owner.reagents.maximum_volume)
|
|
owner.reagents.maximum_volume = volume
|
|
|
|
for(var/rid in reagents)
|
|
var/amount = reagents[rid]
|
|
if(length(tastes) && ispath(rid, /datum/reagent/consumable/nutriment))
|
|
var/datum/reagent/consumable/nutriment/nid = rid
|
|
if(initial(nid.carry_food_tastes))
|
|
owner.reagents.add_reagent(rid, amount, tastes.Copy(), added_purity = reagent_purity)
|
|
continue
|
|
owner.reagents.add_reagent(rid, amount, added_purity = reagent_purity)
|
|
|
|
/datum/component/edible/proc/examine(datum/source, mob/user, list/examine_list)
|
|
SIGNAL_HANDLER
|
|
|
|
var/atom/owner = parent
|
|
if(food_flags & FOOD_NO_EXAMINE)
|
|
return
|
|
if(foodtypes)
|
|
var/list/types = bitfield_to_list(foodtypes, FOOD_FLAGS)
|
|
examine_list += span_notice("It is [LOWER_TEXT(english_list(types))].")
|
|
|
|
var/quality = get_perceived_food_quality(user)
|
|
if(quality > 0)
|
|
var/quality_label = GLOB.food_quality_description[quality]
|
|
examine_list += span_green("You find this meal [quality_label].")
|
|
else if (quality == 0)
|
|
examine_list += span_notice("You find this meal edible.")
|
|
else if (quality <= FOOD_QUALITY_DANGEROUS)
|
|
examine_list += span_warning("You may die from eating this meal.")
|
|
else if (quality <= TOXIC_FOOD_QUALITY_THRESHOLD)
|
|
examine_list += span_warning("You find this meal disgusting!")
|
|
else
|
|
examine_list += span_warning("You find this meal inedible.")
|
|
|
|
if(owner.reagents.total_volume > 0)
|
|
var/purity = owner.reagents.get_average_purity(/datum/reagent/consumable)
|
|
switch(purity)
|
|
if(0 to 0.2)
|
|
examine_list += span_warning("It is made of terrible ingredients shortening the effect...")
|
|
if(0.2 to 0.4)
|
|
examine_list += span_warning("It is made of synthetic ingredients shortening the effect.")
|
|
if(0.4 to 0.6)
|
|
examine_list += span_notice("It is made of average quality ingredients.")
|
|
if(0.6 to 0.8)
|
|
examine_list += span_green("It is made of organic ingredients prolonging the effect.")
|
|
if(0.8 to 1)
|
|
examine_list += span_green("It is made of finest ingredients prolonging the effect!")
|
|
|
|
var/datum/mind/mind = user.mind
|
|
if(mind && HAS_TRAIT_FROM(owner, TRAIT_FOOD_CHEF_MADE, REF(mind)))
|
|
examine_list += span_green("[owner] was made by you!")
|
|
|
|
if(!(food_flags & FOOD_IN_CONTAINER))
|
|
switch(bitecount)
|
|
if(0)
|
|
pass()
|
|
if(1)
|
|
examine_list += span_notice("[owner] was bitten by someone!")
|
|
if(2, 3)
|
|
examine_list += span_notice("[owner] was bitten [bitecount] times!")
|
|
else
|
|
examine_list += span_notice("[owner] was bitten multiple times!")
|
|
|
|
if(GLOB.Debug2)
|
|
examine_list += span_notice("Reagent purities:")
|
|
for(var/datum/reagent/reagent as anything in owner.reagents.reagent_list)
|
|
examine_list += span_notice("- [reagent.name] [reagent.volume]u: [round(reagent.purity * 100)]% pure")
|
|
|
|
if(!HAS_TRAIT(user, TRAIT_REMOTE_TASTING))
|
|
return
|
|
var/fraction = min(bite_consumption / owner.reagents.total_volume, 1)
|
|
checkLiked(fraction, user)
|
|
if (!owner.reagents.get_reagent_amount(/datum/reagent/consumable/salt))
|
|
examine_list += span_notice("It could use a little more Sodium Chloride...")
|
|
if (isliving(user))
|
|
var/mob/living/living_user = user
|
|
living_user.taste_container(owner.reagents)
|
|
|
|
/datum/component/edible/proc/UseFromHand(obj/item/source, mob/living/M, mob/living/user)
|
|
SIGNAL_HANDLER
|
|
|
|
return TryToEat(M, user)
|
|
|
|
/datum/component/edible/proc/TryToEatIt(datum/source, mob/user)
|
|
SIGNAL_HANDLER
|
|
|
|
if (!in_range(source, user))
|
|
return
|
|
return TryToEat(user, user)
|
|
|
|
///Called when food is created through processing (Usually this means it was sliced). We use this to pass the OG items reagents.
|
|
/datum/component/edible/proc/created_by_processing(datum/source, atom/original_atom, list/chosen_processing_option)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!original_atom.reagents)
|
|
return
|
|
|
|
var/atom/this_food = parent
|
|
|
|
//Make sure we have a reagent container large enough to fit the original atom's reagents.
|
|
var/volume = ROUND_UP(original_atom.reagents.maximum_volume / chosen_processing_option[TOOL_PROCESSING_AMOUNT])
|
|
|
|
this_food.create_reagents(volume, this_food.reagents?.flags)
|
|
original_atom.reagents.copy_to(this_food, original_atom.reagents.total_volume / chosen_processing_option[TOOL_PROCESSING_AMOUNT], 1)
|
|
|
|
if(original_atom.name != initial(original_atom.name))
|
|
this_food.name = "slice of [original_atom.name]"
|
|
if(original_atom.desc != initial(original_atom.desc))
|
|
this_food.desc = "[original_atom.desc]"
|
|
|
|
///Called when food is crafted through a crafting recipe datum.
|
|
/datum/component/edible/proc/OnCraft(datum/source, list/components, datum/crafting_recipe/food/recipe)
|
|
SIGNAL_HANDLER
|
|
|
|
var/atom/this_food = parent
|
|
for(var/obj/item/food/crafted_part in components)
|
|
if(!crafted_part.reagents)
|
|
continue
|
|
this_food.reagents.maximum_volume += crafted_part.reagents.maximum_volume
|
|
crafted_part.reagents.trans_to(this_food.reagents, crafted_part.reagents.maximum_volume)
|
|
|
|
this_food.reagents.maximum_volume = ROUND_UP(this_food.reagents.maximum_volume) // Just because I like whole numbers for this.
|
|
|
|
BLACKBOX_LOG_FOOD_MADE(parent.type)
|
|
|
|
///Makes sure the thing hasn't been destroyed or fully eaten to prevent eating phantom edibles
|
|
/datum/component/edible/proc/IsFoodGone(atom/owner, mob/living/feeder)
|
|
if(QDELETED(owner) || !(IS_EDIBLE(owner)))
|
|
return TRUE
|
|
if(owner.reagents.total_volume)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Normal time to forcefeed someone something
|
|
#define EAT_TIME_FORCE_FEED (3 SECONDS)
|
|
/// Multiplier for eat time if the eater has TRAIT_VORACIOUS
|
|
#define EAT_TIME_VORACIOUS_MULT 0.65 // voracious folk eat 35% faster
|
|
/// Multiplier for how much longer it takes a voracious folk to eat while full
|
|
#define EAT_TIME_VORACIOUS_FULL_MULT 4 // Takes at least 4 times as long to eat while full, so dorks cant just clear out the kitchen before they get robusted
|
|
|
|
///All the checks for the act of eating itself and
|
|
/datum/component/edible/proc/TryToEat(mob/living/eater, mob/living/feeder)
|
|
|
|
set waitfor = FALSE
|
|
|
|
var/atom/owner = parent
|
|
|
|
if(feeder.combat_mode)
|
|
return
|
|
|
|
. = COMPONENT_CANCEL_ATTACK_CHAIN //Point of no return I suppose
|
|
|
|
if(IsFoodGone(owner, feeder))
|
|
return
|
|
|
|
if(!CanConsume(eater, feeder))
|
|
return
|
|
var/fullness = eater.get_fullness() + 10 //The theoretical fullness of the person eating if they were to eat this
|
|
|
|
var/time_to_eat = (eater == feeder) ? eat_time : EAT_TIME_FORCE_FEED
|
|
if(HAS_TRAIT(eater, TRAIT_VORACIOUS) && !HAS_TRAIT(eater, TRAIT_GLUTTON)) //with TRAIT_GLUTTON you consume food without delay
|
|
if(fullness < NUTRITION_LEVEL_FAT || (eater != feeder)) // No extra delay when being forcefed
|
|
time_to_eat *= EAT_TIME_VORACIOUS_MULT
|
|
else
|
|
time_to_eat *= (fullness / NUTRITION_LEVEL_FAT) * EAT_TIME_VORACIOUS_FULL_MULT // takes longer to eat the more well fed you are
|
|
if(eater == feeder)//If you're eating it yourself.
|
|
if(eat_time > 0 && !do_after(feeder, time_to_eat, eater, timed_action_flags = food_flags & FOOD_FINGER_FOOD ? IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE : NONE)) //Gotta pass the minimal eat time
|
|
return
|
|
if(IsFoodGone(owner, feeder))
|
|
return
|
|
var/eatverb = pick(eatverbs)
|
|
|
|
var/message_to_nearby_audience = ""
|
|
var/message_to_consumer = ""
|
|
var/message_to_blind_consumer = ""
|
|
|
|
if(junkiness && eater.satiety < -150 && eater.nutrition > NUTRITION_LEVEL_STARVING + 50 && !HAS_TRAIT(eater, TRAIT_VORACIOUS) && !HAS_TRAIT(eater, TRAIT_GLUTTON))
|
|
to_chat(eater, span_warning("You don't feel like eating any more junk food at the moment!"))
|
|
return
|
|
else if(fullness > (600 * (1 + eater.overeatduration / (4000 SECONDS)))) // The more you eat - the more you can eat
|
|
if(HAS_TRAIT(eater, TRAIT_VORACIOUS) || HAS_TRAIT(eater, TRAIT_GLUTTON))
|
|
message_to_nearby_audience = span_notice("[eater] voraciously forces \the [parent] down [eater.p_their()] throat.")
|
|
message_to_consumer = span_notice("You voraciously force \the [parent] down your throat.")
|
|
else
|
|
message_to_nearby_audience = span_warning("[eater] cannot force any more of \the [parent] to go down [eater.p_their()] throat!")
|
|
message_to_consumer = span_warning("You cannot force any more of \the [parent] to go down your throat!")
|
|
message_to_blind_consumer = message_to_consumer
|
|
eater.show_message(message_to_consumer, MSG_VISUAL, message_to_blind_consumer)
|
|
eater.visible_message(message_to_nearby_audience, ignored_mobs = eater)
|
|
//if we're too full, return because we can't eat whatever it is we're trying to eat
|
|
return
|
|
else if(fullness > 500)
|
|
if(HAS_TRAIT(eater, TRAIT_VORACIOUS))
|
|
message_to_nearby_audience = span_notice("[eater] [eatverb]s \the [parent].")
|
|
message_to_consumer = span_notice("You [eatverb] \the [parent].")
|
|
else
|
|
message_to_nearby_audience = span_notice("[eater] unwillingly [eatverb]s a bit of \the [parent].")
|
|
message_to_consumer = span_notice("You unwillingly [eatverb] a bit of \the [parent].")
|
|
else if(fullness > 150)
|
|
message_to_nearby_audience = span_notice("[eater] [eatverb]s \the [parent].")
|
|
message_to_consumer = span_notice("You [eatverb] \the [parent].")
|
|
else if(fullness > 50)
|
|
message_to_nearby_audience = span_notice("[eater] hungrily [eatverb]s \the [parent].")
|
|
message_to_consumer = span_notice("You hungrily [eatverb] \the [parent].")
|
|
else
|
|
message_to_nearby_audience = span_notice("[eater] hungrily [eatverb]s \the [parent], gobbling it down!")
|
|
message_to_consumer = span_notice("You hungrily [eatverb] \the [parent], gobbling it down!")
|
|
|
|
//if we're blind, we want to feel how hungrily we ate that food
|
|
message_to_blind_consumer = message_to_consumer
|
|
eater.show_message(message_to_consumer, MSG_VISUAL, message_to_blind_consumer)
|
|
eater.visible_message(message_to_nearby_audience, ignored_mobs = eater)
|
|
|
|
else //If you're feeding it to someone else.
|
|
if(isbrain(eater))
|
|
to_chat(feeder, span_warning("[eater] doesn't seem to have a mouth!"))
|
|
return
|
|
if(fullness <= (600 * (1 + eater.overeatduration / (2000 SECONDS))) || HAS_TRAIT(eater, TRAIT_VORACIOUS))
|
|
eater.visible_message(
|
|
span_danger("[feeder] attempts to [eater.get_bodypart(BODY_ZONE_HEAD) ? "feed [eater] [parent]." : "stuff [parent] down [eater]'s throat hole! Gross."]"),
|
|
span_userdanger("[feeder] attempts to [eater.get_bodypart(BODY_ZONE_HEAD) ? "feed you [parent]." : "stuff [parent] down your throat hole! Gross."]")
|
|
)
|
|
if(eater.is_blind())
|
|
to_chat(eater, span_userdanger("You feel someone trying to feed you something!"))
|
|
else
|
|
eater.visible_message(
|
|
span_danger("[feeder] cannot force any more of [parent] down [eater]'s [eater.get_bodypart(BODY_ZONE_HEAD) ? "throat!" : "throat hole! Eugh."]"),
|
|
span_userdanger("[feeder] cannot force any more of [parent] down your [eater.get_bodypart(BODY_ZONE_HEAD) ? "throat!" : "throat hole! Eugh."]")
|
|
)
|
|
if(eater.is_blind())
|
|
to_chat(eater, span_userdanger("You're too full to eat what's being fed to you!"))
|
|
return
|
|
if(!do_after(feeder, delay = time_to_eat, target = eater)) //Wait 3-ish seconds before you can feed
|
|
return
|
|
if(IsFoodGone(owner, feeder))
|
|
return
|
|
log_combat(feeder, eater, "fed", owner.reagents.get_reagent_log_string())
|
|
eater.visible_message(
|
|
span_danger("[feeder] forces [eater] to eat [parent]!"),
|
|
span_userdanger("[feeder] forces you to eat [parent]!")
|
|
)
|
|
if(eater.is_blind())
|
|
to_chat(eater, span_userdanger("You're forced to eat something!"))
|
|
|
|
TakeBite(eater, feeder)
|
|
|
|
//If we're not force-feeding and there's an eat delay, try take another bite
|
|
if(eater == feeder && eat_time > 0)
|
|
INVOKE_ASYNC(src, PROC_REF(TryToEat), eater, feeder)
|
|
|
|
#undef EAT_TIME_FORCE_FEED
|
|
#undef EAT_TIME_VORACIOUS_MULT
|
|
#undef EAT_TIME_VORACIOUS_FULL_MULT
|
|
|
|
///This function lets the eater take a bite and transfers the reagents to the eater.
|
|
/datum/component/edible/proc/TakeBite(mob/living/eater, mob/living/feeder)
|
|
|
|
var/atom/owner = parent
|
|
|
|
if(!owner.reagents)
|
|
stack_trace("[eater] failed to bite [owner], because [owner] had no reagents.")
|
|
return FALSE
|
|
if(eater.satiety > -200)
|
|
eater.satiety -= junkiness
|
|
playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE)
|
|
if(!owner.reagents.total_volume)
|
|
return
|
|
var/sig_return = SEND_SIGNAL(parent, COMSIG_FOOD_EATEN, eater, feeder, bitecount, bite_consumption)
|
|
if(sig_return & DESTROY_FOOD)
|
|
qdel(owner)
|
|
return
|
|
|
|
//Give a buff when the dish is hand-crafted and unbitten
|
|
if(bitecount == 0)
|
|
apply_buff(eater)
|
|
|
|
var/fraction = 0.3
|
|
fraction = min(bite_consumption / owner.reagents.total_volume, 1)
|
|
owner.reagents.trans_to(eater, bite_consumption, transferred_by = feeder, methods = INGEST)
|
|
eater.hud_used?.hunger?.update_hunger_bar()
|
|
bitecount++
|
|
|
|
checkLiked(fraction, eater)
|
|
|
|
if(!owner.reagents.total_volume)
|
|
On_Consume(eater, feeder)
|
|
|
|
//Invoke our after eat callback if it is valid
|
|
after_eat?.Invoke(eater, feeder, bitecount)
|
|
|
|
//Invoke the eater's stomach's after_eat callback if valid
|
|
if(iscarbon(eater))
|
|
var/mob/living/carbon/carbon_eater = eater
|
|
var/obj/item/organ/stomach/stomach = carbon_eater.get_organ_slot(ORGAN_SLOT_STOMACH)
|
|
if(istype(stomach))
|
|
stomach.after_eat(owner)
|
|
|
|
return TRUE
|
|
|
|
///Checks whether or not the eater can actually consume the food
|
|
/datum/component/edible/proc/CanConsume(mob/living/carbon/eater, mob/living/feeder)
|
|
if(!iscarbon(eater))
|
|
return FALSE
|
|
if(eater.is_mouth_covered())
|
|
eater.balloon_alert(feeder, "mouth is covered!")
|
|
return FALSE
|
|
|
|
var/atom/food = parent
|
|
|
|
if(food.flags_1 & HOLOGRAM_1)
|
|
if(eater == feeder)
|
|
to_chat(eater, span_notice("You try to take a bite out of [food], but it fades away!"))
|
|
else
|
|
to_chat(feeder, span_notice("You try to feed [eater] [food], but it fades away!"))
|
|
|
|
qdel(food)
|
|
return FALSE
|
|
|
|
if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, food) & COMSIG_CARBON_BLOCK_EAT)
|
|
return
|
|
return TRUE
|
|
|
|
///Applies food buffs according to the crafting complexity
|
|
/datum/component/edible/proc/apply_buff(mob/eater)
|
|
var/buff
|
|
var/recipe_complexity = get_recipe_complexity()
|
|
if(recipe_complexity <= 0)
|
|
return
|
|
var/obj/item/food/food = parent
|
|
if(istype(food) && !isnull(food.crafted_food_buff))
|
|
buff = food.crafted_food_buff
|
|
else
|
|
buff = pick_weight(GLOB.food_buffs[min(recipe_complexity, FOOD_COMPLEXITY_5)])
|
|
if(!isnull(buff))
|
|
var/mob/living/living_eater = eater
|
|
var/atom/owner = parent
|
|
var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // buff duration is 100% at average purity of 50%
|
|
var/strength = recipe_complexity
|
|
living_eater.apply_status_effect(buff, timeout_mod, strength)
|
|
|
|
///Check foodtypes to see if we should send a moodlet
|
|
/datum/component/edible/proc/checkLiked(fraction, mob/eater)
|
|
if(last_check_time + 50 > world.time)
|
|
return FALSE
|
|
if(!ishuman(eater))
|
|
return FALSE
|
|
var/mob/living/carbon/human/gourmand = eater
|
|
|
|
if(istype(parent, /obj/item/food))
|
|
var/obj/item/food/food = parent
|
|
if(food.venue_value >= FOOD_PRICE_EXOTIC)
|
|
gourmand.add_mob_memory(/datum/memory/good_food, food = parent)
|
|
|
|
//Bruh this breakfast thing is cringe and shouldve been handled separately from food-types, remove this in the future (Actually, just kill foodtypes in general)
|
|
if((foodtypes & BREAKFAST) && world.time - SSticker.round_start_time < STOP_SERVING_BREAKFAST)
|
|
gourmand.add_mood_event("breakfast", /datum/mood_event/breakfast)
|
|
last_check_time = world.time
|
|
|
|
var/food_quality = get_perceived_food_quality(gourmand)
|
|
if(food_quality <= FOOD_QUALITY_DANGEROUS && gourmand.check_allergic_reaction(foodtypes, chance = 100, histamine_add = 10))
|
|
return
|
|
|
|
if(food_quality <= TOXIC_FOOD_QUALITY_THRESHOLD)
|
|
to_chat(gourmand,span_warning("What the hell was that thing?!"))
|
|
gourmand.adjust_disgust(25 + 30 * fraction)
|
|
gourmand.add_mood_event("toxic_food", /datum/mood_event/disgusting_food)
|
|
return
|
|
|
|
if(food_quality < 0)
|
|
to_chat(gourmand,span_notice("That didn't taste very good..."))
|
|
gourmand.adjust_disgust(11 + 15 * fraction)
|
|
gourmand.add_mood_event("gross_food", /datum/mood_event/gross_food)
|
|
return
|
|
|
|
if(food_quality == 0)
|
|
return // meh
|
|
|
|
var/atom/owner = parent
|
|
var/timeout_mod = owner.reagents.get_average_purity(/datum/reagent/consumable) * 2 // mood event duration is 100% at average purity of 50%
|
|
var/datum/mood_event/event = GLOB.food_quality_events[food_quality]
|
|
event = new event.type
|
|
event.timeout *= timeout_mod
|
|
gourmand.add_mood_event("quality_food", event)
|
|
gourmand.adjust_disgust(-5 + -2 * food_quality * fraction)
|
|
var/quality_label = GLOB.food_quality_description[food_quality]
|
|
to_chat(gourmand, span_notice("That's \an [quality_label] meal."))
|
|
|
|
/// Get the complexity of the crafted food
|
|
/datum/component/edible/proc/get_recipe_complexity()
|
|
var/list/extra_complexity = list(0)
|
|
SEND_SIGNAL(parent, COMSIG_FOOD_GET_EXTRA_COMPLEXITY, extra_complexity)
|
|
var/complexity_to_add = extra_complexity[1]
|
|
if(!HAS_TRAIT(parent, TRAIT_FOOD_CHEF_MADE) || !istype(parent, /obj/item/food))
|
|
return complexity_to_add // It is factory made. Soulless.
|
|
var/obj/item/food/food = parent
|
|
return food.crafting_complexity + complexity_to_add
|
|
|
|
/// Get food quality adjusted according to eater's preferences
|
|
/datum/component/edible/proc/get_perceived_food_quality(mob/living/eater)
|
|
var/food_quality = get_recipe_complexity()
|
|
var/list/extra_quality = list()
|
|
SEND_SIGNAL(eater, COMSIG_LIVING_GET_PERCEIVED_FOOD_QUALITY, src, extra_quality)
|
|
for(var/quality in extra_quality)
|
|
food_quality += quality
|
|
|
|
if(HAS_TRAIT(parent, TRAIT_FOOD_SILVER)) // it's not real food
|
|
if(!isjellyperson(eater)) //if you aren't a jellyperson, it makes you sick no matter how nice it looks
|
|
return TOXIC_FOOD_QUALITY_THRESHOLD
|
|
food_quality += LIKED_FOOD_QUALITY_CHANGE
|
|
|
|
if(check_liked) //Callback handling; use this as an override for special food like donuts
|
|
var/special_reaction = check_liked.Invoke(eater)
|
|
switch(special_reaction) //return early for special foods
|
|
if(FOOD_LIKED)
|
|
return LIKED_FOOD_QUALITY_CHANGE
|
|
if(FOOD_DISLIKED)
|
|
return DISLIKED_FOOD_QUALITY_CHANGE
|
|
if(FOOD_TOXIC)
|
|
return TOXIC_FOOD_QUALITY_THRESHOLD
|
|
if(FOOD_ALLERGIC)
|
|
return FOOD_QUALITY_DANGEROUS
|
|
|
|
if(ishuman(eater))
|
|
if(foodtypes & eater.get_allergic_foodtypes())
|
|
return FOOD_QUALITY_DANGEROUS
|
|
if(count_matching_foodtypes(foodtypes, eater.get_toxic_foodtypes())) //if the food is toxic, we don't care about anything else
|
|
return TOXIC_FOOD_QUALITY_THRESHOLD
|
|
if(HAS_TRAIT(eater, TRAIT_AGEUSIA)) //if you can't taste it, it doesn't taste good
|
|
return 0
|
|
food_quality += DISLIKED_FOOD_QUALITY_CHANGE * count_matching_foodtypes(foodtypes, eater.get_disliked_foodtypes())
|
|
food_quality += LIKED_FOOD_QUALITY_CHANGE * count_matching_foodtypes(foodtypes, eater.get_liked_foodtypes())
|
|
|
|
return min(food_quality, FOOD_QUALITY_TOP)
|
|
|
|
/// Get the number of matching food types in provided bitfields
|
|
/datum/component/edible/proc/count_matching_foodtypes(bitfield_one, bitfield_two)
|
|
var/count = 0
|
|
var/matching_bits = bitfield_one & bitfield_two
|
|
while (matching_bits > 0)
|
|
if (matching_bits & 1)
|
|
count++
|
|
matching_bits >>= 1
|
|
return count
|
|
|
|
///Delete the item when it is fully eaten
|
|
/datum/component/edible/proc/On_Consume(mob/living/eater, mob/living/feeder)
|
|
SEND_SIGNAL(parent, COMSIG_FOOD_CONSUMED, eater, feeder)
|
|
SEND_SIGNAL(eater, COMSIG_LIVING_FINISH_EAT, parent, feeder)
|
|
|
|
on_consume?.Invoke(eater, feeder)
|
|
if (QDELETED(parent)) // might be destroyed by the callback
|
|
return
|
|
|
|
to_chat(feeder, span_warning("There is nothing left of [parent], oh no!"))
|
|
if(isturf(parent))
|
|
var/turf/T = parent
|
|
T.ScrapeAway(1, CHANGETURF_INHERIT_AIR)
|
|
else
|
|
qdel(parent)
|
|
|
|
///Ability to feed food to puppers
|
|
/datum/component/edible/proc/UseByAnimal(datum/source, mob/living/basic/pet/dog/doggy)
|
|
SIGNAL_HANDLER
|
|
|
|
if(!isdog(doggy) || (food_flags & FOOD_NO_BITECOUNT)) //this entirely relies on bitecounts alas
|
|
return
|
|
|
|
var/atom/food = parent
|
|
|
|
if(food.flags_1 & HOLOGRAM_1)
|
|
to_chat(doggy, span_notice("You try to take a bite out of [food], but it fades away!"))
|
|
qdel(food)
|
|
return
|
|
|
|
if(bitecount == 0 || prob(50))
|
|
doggy.manual_emote("nibbles away at \the [food].")
|
|
bitecount++
|
|
. = COMPONENT_CANCEL_ATTACK_CHAIN
|
|
|
|
doggy.taste_container(food.reagents) // why should carbons get all the fun?
|
|
if(bitecount >= 5)
|
|
var/satisfaction_text = pick("burps from enjoyment.", "yaps for more!", "woofs twice.", "looks at the area where \the [food] was.")
|
|
doggy.manual_emote(satisfaction_text)
|
|
qdel(food)
|
|
|
|
///Ability to feed food to puppers
|
|
/datum/component/edible/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
|
|
SIGNAL_HANDLER
|
|
SEND_SIGNAL(parent, COMSIG_FOOD_CROSSED, arrived, bitecount)
|
|
|
|
///Response to being used to customize something
|
|
/datum/component/edible/proc/used_to_customize(datum/source, atom/customized)
|
|
SIGNAL_HANDLER
|
|
|
|
SEND_SIGNAL(customized, COMSIG_FOOD_INGREDIENT_ADDED, src)
|
|
|
|
///Response to an edible ingredient being added to parent.
|
|
/datum/component/edible/proc/edible_ingredient_added(datum/source, datum/component/edible/ingredient)
|
|
SIGNAL_HANDLER
|
|
|
|
InheritComponent(ingredient, TRUE)
|
|
|
|
/// Response to oozes trying to eat something edible
|
|
/datum/component/edible/proc/on_ooze_eat(datum/source, mob/eater, edible_flags)
|
|
SIGNAL_HANDLER
|
|
|
|
var/atom/food = parent
|
|
|
|
if(food.flags_1 & HOLOGRAM_1)
|
|
to_chat(eater, span_notice("You try to take a bite out of [food], but it fades away!"))
|
|
qdel(food)
|
|
return COMPONENT_ATOM_EATEN
|
|
|
|
if(foodtypes & edible_flags)
|
|
food.reagents.trans_to(eater, food.reagents.total_volume, transferred_by = eater)
|
|
eater.visible_message(span_warning("[eater] eats [food]!"), span_notice("You eat [food]."))
|
|
playsound(get_turf(eater),'sound/items/eatfood.ogg', rand(30,50), TRUE)
|
|
qdel(food)
|
|
return COMPONENT_ATOM_EATEN
|
|
|
|
#define REQUIRED_MAT_FLAGS (MATERIAL_EFFECTS|MATERIAL_NO_EDIBILITY)
|
|
|
|
///Calls on_edible_applied() for the main material composing the atom parent
|
|
/datum/component/edible/proc/on_material_effects(atom/source, list/materials, datum/material/main_material)
|
|
SIGNAL_HANDLER
|
|
if((source.material_flags & REQUIRED_MAT_FLAGS) == REQUIRED_MAT_FLAGS)
|
|
main_material.on_edible_applied(source, src)
|
|
|
|
///Calls on_edible_removed() for the main material no longer composing the atom parent
|
|
/datum/component/edible/proc/on_remove_material_effects(atom/source, list/materials, datum/material/main_material)
|
|
SIGNAL_HANDLER
|
|
if((source.material_flags & REQUIRED_MAT_FLAGS) == REQUIRED_MAT_FLAGS)
|
|
main_material.on_edible_removed(source, src)
|
|
|
|
#undef REQUIRED_MAT_FLAGS
|
|
#undef DEFAULT_EDIBLE_VOLUME
|