diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm
index 6721ad28997..920b8054445 100644
--- a/code/__DEFINES/living.dm
+++ b/code/__DEFINES/living.dm
@@ -10,6 +10,8 @@
* update_body_parts() is going to be called ONE time once everything is done.
*/
#define STOP_OVERLAY_UPDATE_BODY_PARTS (1<<2)
+/// Nutrition changed last life tick, so we should bulk update this tick
+#define QUEUE_NUTRITION_UPDATE (1<<3)
/// Getter for a mob/living's lying angle, otherwise protected
#define GET_LYING_ANGLE(mob) (UNLINT(mob.lying_angle))
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 433a26c6278..772125facc7 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -242,6 +242,7 @@
#define NUTRITION_LEVEL_WELL_FED 450
#define NUTRITION_LEVEL_FED 350
#define NUTRITION_LEVEL_HUNGRY 250
+#define NUTRITION_LEVEL_VERY_HUNGRY 200
#define NUTRITION_LEVEL_STARVING 150
#define NUTRITION_LEVEL_START_MIN 250
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index 837c3eb16ee..aec20020c3e 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -97,7 +97,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list(
var/atom/movable/screen/stamina
var/atom/movable/screen/healthdoll/healthdoll
var/atom/movable/screen/spacesuit
- var/atom/movable/screen/hunger
+ var/atom/movable/screen/hunger/hunger
// subtypes can override this to force a specific UI style
var/ui_style
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 9bcc565f1d9..ff312bb15ca 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -925,26 +925,30 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/splash)
icon_state = "stamina0"
screen_loc = ui_stamina
-#define HUNGER_STATE_FAT 2
-#define HUNGER_STATE_FULL 1
-#define HUNGER_STATE_FINE 0
-#define HUNGER_STATE_HUNGRY -1
-#define HUNGER_STATE_STARVING -2
+#define HUNGER_STATE_FAT 5
+#define HUNGER_STATE_FULL 4
+#define HUNGER_STATE_FINE 3
+#define HUNGER_STATE_HUNGRY 2
+#define HUNGER_STATE_VERY_HUNGRY 1
+#define HUNGER_STATE_STARVING 0
/atom/movable/screen/hunger
name = "hunger"
icon_state = "hungerbar"
- base_icon_state = "hungerbar"
screen_loc = ui_hunger
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
/// What state of hunger are we in?
- VAR_PRIVATE/state = HUNGER_STATE_FINE
+ VAR_PRIVATE/state
+ /// What was the last fullness we recorded?
+ VAR_PRIVATE/fullness
/// What food icon do we show by the bar
var/food_icon = 'icons/obj/food/burgerbread.dmi'
/// What food icon state do we show by the bar
var/food_icon_state = "hburger"
/// The image shown by the bar.
VAR_PRIVATE/image/food_image
+ /// The actual bar
+ VAR_PRIVATE/atom/movable/screen/hunger_bar/hunger_bar
/atom/movable/screen/hunger/Initialize(mapload, datum/hud/hud_owner)
. = ..()
@@ -955,14 +959,18 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/splash)
if(!ishuman(hungry) || CONFIG_GET(flag/disable_human_mood))
screen_loc = ui_mood // Slot in where mood normally is if mood is disabled
+ // Burger next to the bar
food_image = image(icon = food_icon, icon_state = food_icon_state, pixel_x = -5)
food_image.plane = plane
food_image.appearance_flags |= KEEP_APART // To be unaffected by filters applied to src
food_image.add_filter("simple_outline", 2, outline_filter(1, COLOR_BLACK, OUTLINE_SHARP))
underlays += food_image // To be below filters applied to src
- SetInvisibility(INVISIBILITY_ABSTRACT, name) // Start invisible, update later
- update_appearance()
+ // The actual bar
+ hunger_bar = new(src, null)
+ vis_contents += hunger_bar
+
+ update_hunger_bar(instant = TRUE)
/atom/movable/screen/hunger/proc/update_hunger_state()
var/mob/living/hungry = hud?.mymob
@@ -970,61 +978,129 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/splash)
return
if(HAS_TRAIT(hungry, TRAIT_NOHUNGER) || !hungry.get_organ_slot(ORGAN_SLOT_STOMACH))
+ fullness = NUTRITION_LEVEL_FED
state = HUNGER_STATE_FINE
return
-
if(HAS_TRAIT(hungry, TRAIT_FAT))
+ fullness = NUTRITION_LEVEL_FAT
state = HUNGER_STATE_FAT
return
if(HAS_TRAIT(hungry, TRAIT_GLUTTON))
+ fullness = NUTRITION_LEVEL_VERY_HUNGRY
state = HUNGER_STATE_HUNGRY // Can't get enough
return
- switch(hungry.nutrition)
- if(NUTRITION_LEVEL_FULL to INFINITY)
+ fullness = round(hungry.get_fullness(only_consumable = TRUE), 0.05)
+ switch(fullness)
+ if(1 + NUTRITION_LEVEL_FULL to INFINITY)
state = HUNGER_STATE_FULL
- if(NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FULL)
+ if(1 + NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FULL)
state = HUNGER_STATE_FINE
- if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_HUNGRY)
+ if(1 + NUTRITION_LEVEL_VERY_HUNGRY to NUTRITION_LEVEL_HUNGRY)
+ state = HUNGER_STATE_FINE
+ if(1 + NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_VERY_HUNGRY)
state = HUNGER_STATE_HUNGRY
if(0 to NUTRITION_LEVEL_STARVING)
state = HUNGER_STATE_STARVING
/atom/movable/screen/hunger/update_appearance(updates)
+ update_hunger_bar()
+ return ..()
+
+/// Updates the hunger bar's appearance.
+/// If `instant` is TRUE, the bar will update immediately rather than animating.
+/atom/movable/screen/hunger/proc/update_hunger_bar(instant = FALSE)
var/old_state = state
- update_hunger_state() // Do this before we call all the other update procs
- if(state == old_state) // Let's not be wasteful
- return
+ var/old_fullness = fullness
+ update_hunger_state()
+ if(old_state != state || old_fullness != fullness)
+ // Fades out if we ARE "fine" AND if our stomach has no food digesting
+ var/mob/living/hungry = hud?.mymob
+ if(alpha == 255 && (state == HUNGER_STATE_FINE && abs(fullness - hungry.nutrition) < 1))
+ if(instant)
+ alpha = 0
+ else
+ animate(src, alpha = 0, time = 1 SECONDS)
+ // Fades in if we WERE "fine" OR if our stomach has food digesting
+ else if(alpha == 0 && (state != HUNGER_STATE_FINE || abs(fullness - hungry.nutrition) >= 1))
+ if(instant)
+ alpha = 255
+ else
+ animate(src, alpha = 255, time = 1 SECONDS)
+
+ if(old_state != state)
+ // Update filter around the bar
+ if(state == HUNGER_STATE_STARVING)
+ if(!get_filter("hunger_outline"))
+ add_filter("hunger_outline", 1, list("type" = "outline", "color" = "#FF0033", "alpha" = 0, "size" = 2))
+ animate(get_filter("hunger_outline"), alpha = 200, time = 1.5 SECONDS, loop = -1)
+ animate(alpha = 0, time = 1.5 SECONDS)
+
+ else if(old_state == HUNGER_STATE_STARVING)
+ remove_filter("hunger_outline")
+
+ // Update color of the food
+ if((state == HUNGER_STATE_FAT) != (old_state == HUNGER_STATE_FAT))
+ underlays -= food_image
+ food_image.color = state == HUNGER_STATE_FAT ? COLOR_DARK : null
+ underlays += food_image
+
+ // Update hunger bar
+ if(old_fullness != fullness)
+ // instant if invisible OR if instant is set
+ hunger_bar.update_fullness(fullness, alpha == 0 || instant)
+
+/atom/movable/screen/hunger_bar
+ icon_state = "hungerbar_bar"
+ screen_loc = ui_hunger
+ vis_flags = VIS_INHERIT_ID | VIS_INHERIT_PLANE
+ /// Mask
+ VAR_PRIVATE/static/icon/bar_mask
+ /// Gradient used to color the bar
+ VAR_PRIVATE/static/list/hunger_gradient = list(
+ 0.0, "#FF0000",
+ 0.2, "#FF8000",
+ 0.4, "#f0f000",
+ 0.6, "#00FF00",
+ 0.8, "#46daff",
+ 1.0, "#2A72AA",
+ 1.2, "#494949",
+ )
+ /// Offset of the mask
+ VAR_PRIVATE/bar_offset
+ /// Last "fullness" value (rounded) we used to update the bar
+ VAR_PRIVATE/last_fullness_band = -1
+
+/atom/movable/screen/hunger_bar/Initialize(mapload, datum/hud/hud_owner)
. = ..()
- if(state == HUNGER_STATE_FINE)
- SetInvisibility(INVISIBILITY_ABSTRACT, name)
+ var/atom/movable/movable_loc = ismovable(loc) ? loc : null
+ screen_loc = movable_loc?.screen_loc
+ bar_mask ||= icon(icon, "hungerbar_mask")
+
+/atom/movable/screen/hunger_bar/proc/update_fullness(new_fullness, instant)
+ new_fullness = round(new_fullness / NUTRITION_LEVEL_FULL, 0.05)
+ if(new_fullness == last_fullness_band)
return
-
- else if(invisibility)
- RemoveInvisibility(name)
-
- if(state == HUNGER_STATE_STARVING)
- if(!get_filter("hunger_outline"))
- add_filter("hunger_outline", 1, list("type" = "outline", "color" = "#FF0033", "alpha" = 0, "size" = 2))
- animate(get_filter("hunger_outline"), alpha = 200, time = 1.5 SECONDS, loop = -1)
- animate(alpha = 0, time = 1.5 SECONDS)
-
+ last_fullness_band = new_fullness
+ // Update color
+ var/new_color = gradient(hunger_gradient, clamp(new_fullness, 0, 1.2))
+ if(instant)
+ color = new_color
else
- remove_filter("hunger_outline")
-
- // Update color of the food
- if((state == HUNGER_STATE_FAT) != (old_state == HUNGER_STATE_FAT))
- underlays -= food_image
- food_image.color = state == HUNGER_STATE_FAT ? COLOR_DARK : null
- underlays += food_image
-
-/atom/movable/screen/hunger/update_icon_state()
- . = ..()
- icon_state = "[base_icon_state][state]"
+ animate(src, color = new_color, 0.5 SECONDS)
+ // Update mask
+ var/old_bar_offset = bar_offset
+ bar_offset = clamp(-20 + (20 * new_fullness), -20, 0)
+ if(old_bar_offset != bar_offset)
+ if(instant || isnull(old_bar_offset))
+ add_filter("hunger_bar_mask", 1, alpha_mask_filter(0, bar_offset, bar_mask))
+ else
+ transition_filter("hunger_bar_mask", alpha_mask_filter(0, bar_offset), 0.5 SECONDS)
#undef HUNGER_STATE_FAT
-#undef HUNGER_STATE_FULL
#undef HUNGER_STATE_FINE
+#undef HUNGER_STATE_FULL
#undef HUNGER_STATE_HUNGRY
#undef HUNGER_STATE_STARVING
+#undef HUNGER_STATE_VERY_HUNGRY
diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm
index 8faf32fef56..a5e96335c19 100644
--- a/code/datums/components/food/edible.dm
+++ b/code/datums/components/food/edible.dm
@@ -501,6 +501,7 @@ Behavior that's still missing from this component that original food items had t
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)
diff --git a/code/datums/elements/food/foodlike_drink.dm b/code/datums/elements/food/foodlike_drink.dm
index e83468d68a1..e8f6cf49f9e 100644
--- a/code/datums/elements/food/foodlike_drink.dm
+++ b/code/datums/elements/food/foodlike_drink.dm
@@ -38,6 +38,7 @@
return
source.attack(user, user)
+ user.hud_used?.hunger?.update_hunger_bar()
/datum/element/foodlike_drink/proc/can_keep_drinking(obj/item/reagent_containers/source, mob/living/user)
if(QDELETED(source) || user.get_active_held_item() != source)
diff --git a/code/datums/mood.dm b/code/datums/mood.dm
index 5d2e70ec2de..08d493ada1c 100644
--- a/code/datums/mood.dm
+++ b/code/datums/mood.dm
@@ -128,8 +128,10 @@
add_mood_event(MOOD_CATEGORY_NUTRITION, /datum/mood_event/fed)
if(NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FED)
clear_mood_event(MOOD_CATEGORY_NUTRITION)
- if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_HUNGRY)
+ if(NUTRITION_LEVEL_VERY_HUNGRY to NUTRITION_LEVEL_HUNGRY)
add_mood_event(MOOD_CATEGORY_NUTRITION, /datum/mood_event/hungry)
+ if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_VERY_HUNGRY)
+ add_mood_event(MOOD_CATEGORY_NUTRITION, /datum/mood_event/hungry_very)
if(0 to NUTRITION_LEVEL_STARVING)
add_mood_event(MOOD_CATEGORY_NUTRITION, /datum/mood_event/starving)
@@ -339,7 +341,9 @@
msg += "[span_info("I'm not hungry.")]
"
if(NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FED)
msg += "[span_info("I could use a bite to eat.")]
"
- if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_HUNGRY)
+ if(NUTRITION_LEVEL_VERY_HUNGRY to NUTRITION_LEVEL_HUNGRY)
+ msg += "[span_warning("I'm feeling hungry.")]
"
+ if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_VERY_HUNGRY)
msg += "[span_warning("I feel quite hungry.")]
"
if(0 to NUTRITION_LEVEL_STARVING)
msg += "[span_boldwarning("I'm starving!")]
"
diff --git a/code/datums/mood_events/needs_events.dm b/code/datums/mood_events/needs_events.dm
index 66f930fa784..96f35708a36 100644
--- a/code/datums/mood_events/needs_events.dm
+++ b/code/datums/mood_events/needs_events.dm
@@ -17,6 +17,10 @@
/datum/mood_event/hungry
description = "I'm getting a bit hungry."
+ mood_change = -3
+
+/datum/mood_event/hungry_very
+ description = "I'm hungry!"
mood_change = -6
/datum/mood_event/starving
diff --git a/code/modules/mob/living/carbon/human/init_signals.dm b/code/modules/mob/living/carbon/human/init_signals.dm
index a425a5c279f..8e6465cf0f3 100644
--- a/code/modules/mob/living/carbon/human/init_signals.dm
+++ b/code/modules/mob/living/carbon/human/init_signals.dm
@@ -51,7 +51,7 @@
/mob/living/carbon/human/proc/on_fat(datum/source)
SIGNAL_HANDLER
- hud_used?.hunger?.update_appearance()
+ hud_used?.hunger?.update_hunger_bar()
mob_mood?.update_nutrition_moodlets()
if(HAS_TRAIT(src, TRAIT_FAT))
@@ -68,7 +68,7 @@
overeatduration = 0
remove_traits(list(TRAIT_FAT, TRAIT_OFF_BALANCE_TACKLER), OBESITY)
else
- hud_used?.hunger?.update_appearance()
+ hud_used?.hunger?.update_hunger_bar()
mob_mood?.update_nutrition_moodlets()
/// Signal proc for [COMSIG_ATOM_CONTENTS_WEIGHT_CLASS_CHANGED] to check if an item is suddenly too heavy for our pockets
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 3f81b193d0d..d7471e6b7b3 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -686,22 +686,29 @@
//Stomach//
///////////
-/mob/living/carbon/get_fullness()
- var/fullness = nutrition
+/mob/living/carbon/get_fullness(only_consumable)
+ . = ..()
var/obj/item/organ/stomach/belly = get_organ_slot(ORGAN_SLOT_STOMACH)
if(!belly) //nothing to see here if we do not have a stomach
- return fullness
+ return .
- for(var/bile in belly.reagents.reagent_list)
- var/datum/reagent/bits = bile
- if(istype(bits, /datum/reagent/consumable))
- var/datum/reagent/consumable/goodbit = bile
- fullness += goodbit.get_nutriment_factor(src) * goodbit.volume / goodbit.metabolization_rate
+ for(var/datum/reagent/bits as anything in belly.reagents.reagent_list)
+ // hack to get around stomachs having 5u stomach lining reagent ugugugu
+ var/effective_volume = bits.volume
+ if(belly.food_reagents[bits.type])
+ effective_volume -= belly.food_reagents[bits.type]
+ if(effective_volume <= 0)
continue
- fullness += 0.6 * bits.volume / bits.metabolization_rate //not food takes up space
+ if(istype(bits, /datum/reagent/consumable))
+ var/datum/reagent/consumable/goodbit = bits
+ . += goodbit.get_nutriment_factor(src) * effective_volume / goodbit.metabolization_rate
+ continue
+ if(!only_consumable)
+ continue
+ . += 0.6 * effective_volume / bits.metabolization_rate //not food takes up space
- return fullness
+ return .
/mob/living/carbon/has_reagent(reagent, amount = -1, needs_metabolizing = FALSE)
. = ..()
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 59bc1a9f4d8..374de84bb27 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -63,6 +63,11 @@
handle_wounds(seconds_per_tick, times_fired)
+ if(living_flags & QUEUE_NUTRITION_UPDATE)
+ mob_mood?.update_nutrition_moodlets()
+ hud_used?.hunger?.update_hunger_bar()
+ living_flags &= ~QUEUE_NUTRITION_UPDATE
+
if(stat != DEAD)
return 1
@@ -97,18 +102,19 @@
/**
* Get the fullness of the mob
*
- * This returns a value form 0 upwards to represent how full the mob is.
- * The value is a total amount of consumable reagents in the body combined
- * with the total amount of nutrition they have.
- * This does not have an upper limit.
+ * Fullness is a representation of how much nutrition the mob has,
+ * including the nutrition of stuff yet to be digested (reagents in blood / stomach)
+ *
+ * * only_consumable - if TRUE, only consumable reagents are counted.
+ * Otherwise, all reagents contribute to fullness, despite not adding nutrition as they process.
+ *
+ * Returns a number representing fullness, scaled similarly to nutrition.
*/
-/mob/living/proc/get_fullness()
+/mob/living/proc/get_fullness(only_consumable)
var/fullness = nutrition
// we add the nutrition value of what we're currently digesting
- for(var/bile in reagents.reagent_list)
- var/datum/reagent/consumable/bits = bile
- if(bits)
- fullness += bits.get_nutriment_factor(src) * bits.volume / bits.metabolization_rate
+ for(var/datum/reagent/consumable/bits in reagents.reagent_list)
+ fullness += bits.get_nutriment_factor(src) * bits.volume / bits.metabolization_rate
return fullness
/**
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 97f92018200..ec5463b40dd 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1448,11 +1448,15 @@
return
nutrition = max(0, nutrition + change)
- hud_used?.hunger?.update_appearance()
/mob/living/adjust_nutrition(change, forced)
. = ..()
- mob_mood?.update_nutrition_moodlets()
+ // Queue update if change is small enough (6 is 1% of nutrition softcap)
+ if(abs(change) >= 6)
+ mob_mood?.update_nutrition_moodlets()
+ hud_used?.hunger?.update_hunger_bar()
+ else
+ living_flags |= QUEUE_NUTRITION_UPDATE
///Force set the mob nutrition
/mob/proc/set_nutrition(set_to, forced = FALSE) //Seriously fuck you oldcoders.
@@ -1460,11 +1464,16 @@
return
nutrition = max(0, set_to)
- hud_used?.hunger?.update_appearance()
/mob/living/set_nutrition(set_to, forced)
+ var/old_nutrition = nutrition
. = ..()
- mob_mood?.update_nutrition_moodlets()
+ // Queue update if change is small enough (6 is 1% of nutrition softcap)
+ if(abs(old_nutrition - nutrition) >= 6)
+ mob_mood?.update_nutrition_moodlets()
+ hud_used?.hunger?.update_hunger_bar()
+ else
+ living_flags |= QUEUE_NUTRITION_UPDATE
///Apply a proper movespeed modifier based on items we have equipped
/mob/proc/update_equipment_speed_mods()
diff --git a/code/modules/surgery/organs/internal/stomach/_stomach.dm b/code/modules/surgery/organs/internal/stomach/_stomach.dm
index f682a167ac3..53258eec7d2 100644
--- a/code/modules/surgery/organs/internal/stomach/_stomach.dm
+++ b/code/modules/surgery/organs/internal/stomach/_stomach.dm
@@ -384,7 +384,7 @@
/obj/item/organ/stomach/on_mob_insert(mob/living/carbon/receiver, special, movement_flags)
. = ..()
- receiver.hud_used?.hunger?.update_appearance()
+ receiver.hud_used?.hunger?.update_hunger_bar()
RegisterSignal(receiver, COMSIG_CARBON_VOMITED, PROC_REF(on_vomit))
RegisterSignal(receiver, COMSIG_HUMAN_GOT_PUNCHED, PROC_REF(on_punched))
@@ -393,7 +393,7 @@
var/mob/living/carbon/human/human_owner = stomach_owner
human_owner.clear_alert(ALERT_DISGUST)
human_owner.clear_mood_event("disgust")
- stomach_owner.hud_used?.hunger?.update_appearance()
+ stomach_owner.hud_used?.hunger?.update_hunger_bar()
UnregisterSignal(stomach_owner, list(COMSIG_CARBON_VOMITED, COMSIG_HUMAN_GOT_PUNCHED))
return ..()
diff --git a/icons/hud/screen_gen.dmi b/icons/hud/screen_gen.dmi
index c38725e43e4..f77266a28f1 100644
Binary files a/icons/hud/screen_gen.dmi and b/icons/hud/screen_gen.dmi differ