diff --git a/aurorastation.dme b/aurorastation.dme
index c88d1bdfec5..4fa9333c9c7 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -1773,7 +1773,6 @@
#include "code\modules\mob\living\carbon\carbon.dm"
#include "code\modules\mob\living\carbon\carbon_defense.dm"
#include "code\modules\mob\living\carbon\carbon_defines.dm"
-#include "code\modules\mob\living\carbon\carbon_powers.dm"
#include "code\modules\mob\living\carbon\diona_base.dm"
#include "code\modules\mob\living\carbon\give.dm"
#include "code\modules\mob\living\carbon\resist.dm"
@@ -1805,6 +1804,7 @@
#include "code\modules\mob\living\carbon\brain\say.dm"
#include "code\modules\mob\living\carbon\human\appearance.dm"
#include "code\modules\mob\living\carbon\human\death.dm"
+#include "code\modules\mob\living\carbon\human\devour.dm"
#include "code\modules\mob\living\carbon\human\diona_gestalt.dm"
#include "code\modules\mob\living\carbon\human\emote.dm"
#include "code\modules\mob\living\carbon\human\examine.dm"
diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm
index 02fa9e20e0d..36a0f6ccc70 100644
--- a/code/__defines/mobs.dm
+++ b/code/__defines/mobs.dm
@@ -209,11 +209,13 @@
#define GLUT_TINY 1 // Eat anything tiny and smaller
#define GLUT_SMALLER 2 // Eat anything smaller than we are
#define GLUT_ANYTHING 4 // Eat anything, ever
+#define GLUT_MESSY 8 // Only eat mobs, and eat them in chunks.
+
+#define GLUT_ITEM_TINY 16 // Eat items with a w_class of small or smaller
+#define GLUT_ITEM_NORMAL 32 // Eat items with a w_class of normal or smaller
+#define GLUT_ITEM_ANYTHING 64 // Eat any item
+#define GLUT_PROJECTILE_VOMIT 128 // When vomitting, does it fly out?
-#define GLUT_ITEM_TINY 8 // Eat items with a w_class of small or smaller
-#define GLUT_ITEM_NORMAL 16 // Eat items with a w_class of normal or smaller
-#define GLUT_ITEM_ANYTHING 32 // Eat any item
-#define GLUT_PROJECTILE_VOMIT 64 // When vomitting, does it fly out?
// Devour speeds, returned by can_devour()
#define DEVOUR_SLOW 1
diff --git a/code/modules/mob/living/carbon/carbon_powers.dm b/code/modules/mob/living/carbon/carbon_powers.dm
deleted file mode 100644
index 6ec908e4606..00000000000
--- a/code/modules/mob/living/carbon/carbon_powers.dm
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Attempt to devour victim
- *
- * Returns TRUE on success, FALSE on failure
- */
-/mob/living/carbon/proc/devour(atom/movable/victim)
- var/can_eat = can_devour(victim)
- if(!can_eat)
- return FALSE
-
- var/eat_speed = 100
- if(can_eat == DEVOUR_FAST)
- eat_speed = 30
- src.visible_message("\The [src] is attempting to devour \the [victim] whole!")
- var/mob/target = victim
- if(isobj(victim))
- target = src
- if(!do_mob(src,target,eat_speed))
- return FALSE
- src.visible_message("\The [src] devours \the [victim] whole!")
- if(ismob(victim))
- admin_attack_log(src, victim, "Devoured.", "Was devoured by.", "devoured")
- else
- src.drop_from_inventory(victim)
- move_to_stomach(victim)
-
- return TRUE
-
-/mob/living/carbon/proc/move_to_stomach(atom/movable/victim)
- return
diff --git a/code/modules/mob/living/carbon/human/devour.dm b/code/modules/mob/living/carbon/human/devour.dm
new file mode 100644
index 00000000000..819bb74b740
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/devour.dm
@@ -0,0 +1,189 @@
+ #define PPM 9 //Protein per meat, used for calculating the quantity of protein in an animal
+
+/**
+ * Attempt to devour victim
+ *
+ * Returns TRUE on success, FALSE on failure
+ */
+/mob/living/carbon/human/proc/devour(atom/movable/victim)
+ var/can_eat = can_devour(victim)
+ if(!can_eat)
+ return FALSE
+
+ var/eat_speed = 100
+ if(can_eat == DEVOUR_FAST)
+ eat_speed = 30
+ if((species?.gluttonous & GLUT_MESSY) && ismob(victim))
+ eat_speed *= 3 //We want our bites to take a lot so as to not spam the chat.
+ do_gradual_devour(victim, eat_speed)
+ return
+ src.visible_message("\The [src] is attempting to devour \the [victim] whole!")
+ var/mob/target = victim
+ if(isobj(victim))
+ target = src
+ if(!do_mob(src,target,eat_speed))
+ return FALSE
+ src.visible_message("\The [src] devours \the [victim] whole!")
+ if(ismob(victim))
+ admin_attack_log(src, victim, "Devoured.", "Was devoured by.", "devoured")
+ else
+ src.drop_from_inventory(victim)
+ move_to_stomach(victim)
+
+ return TRUE
+
+/mob/living/carbon/human/proc/move_to_stomach(atom/movable/victim)
+ return
+
+// Snowflake procs for unathi. Because lore said so.
+
+/mob/living/carbon/human/proc/do_gradual_devour(var/mob/living/victim, var/eat_speed)
+ set waitfor = FALSE
+
+ //This function will start consuming the victim by taking bites out of them.
+ //Victim or attacker moving will interrupt it
+ //A bite will be taken every 4 seconds
+ var/bite_delay = 4
+ var/bite_size = mouth_size * 0.5
+ var/num_bites_needed = (victim.mob_size * 2)/bite_size //Total bites needed to eat it from full health
+ var/PEPB = 1/num_bites_needed //Percentage eaten per bite
+ var/turf/ourloc = src.loc
+ var/turf/victimloc = victim.loc
+ var/messes = 0 //Number of bloodstains we've placed
+ var/datum/reagents/vessel = victim.get_vessel(1)
+ if(!victim.composition_reagent_quantity)
+ victim.calculate_composition()
+ var/dam_multiplier = 1
+
+ var/victim_maxhealth = victim.maxHealth //We cache this here in case we need to edit it.
+
+ var/damage_dealt = victim_maxhealth * PEPB * dam_multiplier
+
+ //Now, incase we're resuming an earlier feeding session on the same creature
+ //We calculate the actual bites needed to fully eat it based on how eaten it already is
+ if(victim.getBruteLoss())
+ var/percentageDamaged = victim.getBruteLoss() / victim_maxhealth
+ var/percentageRemaining = 1 - percentageDamaged
+ num_bites_needed = percentageRemaining / PEPB
+
+ var/time_needed_seconds = num_bites_needed*bite_delay//in seconds for now
+ var/time_needed_minutes
+ var/time_needed_string
+ if (time_needed_seconds > 60)
+ time_needed_minutes = round((time_needed_seconds/60))
+ time_needed_seconds = time_needed_seconds % 60
+ time_needed_string = "[time_needed_minutes] minutes and [time_needed_seconds] seconds"
+ else
+ time_needed_string = "[time_needed_seconds] seconds"
+
+ src.visible_message("[src] starts to devour [victim]!", \
+ "You start devouring [victim], this will take approximately [time_needed_string]. You and the victim must remain still to continue, \
+ but you can interrupt feeding anytime and leave with what you've already eaten.")
+
+ for (var/i = 0 to num_bites_needed)
+ if(do_mob(src, victim, bite_delay * 10, extra_checks = CALLBACK(src, .proc/devouring_equals, victim)))
+ face_atom(victim)
+ victim.apply_damage(damage_dealt, BRUTE)
+ var/obj/item/organ/internal/stomach/S = internal_organs_by_name[BP_STOMACH]
+ if(S)
+ S.ingested.add_reagent(victim.composition_reagent, (victim.composition_reagent_quantity * 0.5) * PEPB)
+ visible_message("[src] bites a chunk out of [victim]!","[bitemessage(victim)]")
+ if(messes < victim.mob_size - 1 && prob(50))
+ handle_devour_mess(src, victim, vessel)
+ if(victim.getBruteLoss() >= victim_maxhealth)
+ visible_message("[src] finishes devouring [victim].","You finish devouring [victim].")
+ handle_devour_mess(src, victim, vessel, 1)
+ qdel(victim)
+ else
+ if(nutrition >= (max_nutrition - 60))
+ to_chat(src, "Your stomach is full!")
+ if (victim && !QDELETED(victim) && victimloc != victim.loc) //This procs when the victim gets moved to nullspace.
+ to_chat(src, "[victim] moved away, you need to keep it still. Try grabbing, stunning or killing it first.")
+ else if (ourloc != src.loc)
+ to_chat(src, "You moved! Devouring cancelled.")
+ break
+
+/mob/living/carbon/human/proc/devouring_equals(var/mob/victim)
+ for(var/obj/item/grab/G in victim.grabbed_by)
+ if(G && G.state >= GRAB_NECK)
+ return TRUE
+ return FALSE
+
+//Helpers
+/proc/bitemessage(var/mob/living/victim)
+ return pick(
+ "You take a bite out of \the [victim].",
+ "You rip a chunk off of \the [victim].",
+ "You consume a piece of \the [victim].",
+ "You feast upon your prey.",
+ "You chow down on \the [victim].",
+ "You gobble \the [victim]'s flesh.")
+
+/proc/handle_devour_mess(var/mob/user, var/mob/living/victim, var/datum/reagents/vessel, var/finish = 0)
+ //The maximum number of blood placements is equal to the mob size of the victim
+ //We will use one blood placement on each of the following, in this order
+ //Bloodying the victim's tile
+ //Bloodying the attacker, if possible
+ //Bloodying the attacker's tile
+ //After that, we will allocate the remaining blood placements to random tiles around the victim and attacker, until either all are used or victim is dead
+ var/datum/reagent/blood/B = vessel.get_master_reagent()
+
+ if (!turf_hasblood(get_turf(victim)))
+ devour_add_blood(victim, get_turf(victim), vessel)
+ return 1
+
+ else if (istype(user, /mob/living/carbon/human) && !user.blood_DNA)
+ //if this blood isn't already in the list, add it
+ user.blood_DNA = list(B.data["blood_DNA"])
+ user.blood_color = B.data["blood_color"]
+ user.update_inv_gloves() //handles bloody hands overlays and updating
+ user.verbs += /mob/living/carbon/human/proc/bloody_doodle
+ return 1
+
+ else if (!turf_hasblood(get_turf(user)))
+ devour_add_blood(victim, get_turf(user), vessel)
+ return 1
+
+ if (finish)
+ //A bigger victim makes more gibs
+ if (victim.mob_size >= 3)
+ new /obj/effect/decal/cleanable/blood/gibs(get_turf(victim))
+ if (victim.mob_size >= 5)
+ new /obj/effect/decal/cleanable/blood/gibs(get_turf(victim))
+ if (victim.mob_size >= 7)
+ new /obj/effect/decal/cleanable/blood/gibs(get_turf(victim))
+ if (victim.mob_size >= 9)
+ new /obj/effect/decal/cleanable/blood/gibs(get_turf(victim))
+ return 1
+ return 0
+
+/proc/devour_add_blood(var/mob/living/M, var/turf/location, var/datum/reagents/vessel)
+ for(var/datum/reagent/blood/source in vessel.reagent_list)
+ var/obj/effect/decal/cleanable/blood/B = new /obj/effect/decal/cleanable/blood(location)
+
+ // Update appearance.
+ if(source.data["blood_colour"])
+ B.basecolor = source.data["blood_colour"]
+ B.update_icon()
+
+ // Update blood information.
+ if(source.data["blood_DNA"])
+ B.blood_DNA = list()
+ if(source.data["blood_type"])
+ B.blood_DNA[source.data["blood_DNA"]] = source.data["blood_type"]
+ else
+ B.blood_DNA[source.data["blood_DNA"]] = "O+"
+
+ // Update virus information.
+ if(source.data["virus2"])
+ B.virus2 = virus_copylist(source.data["virus2"])
+
+ B.fluorescent = 0
+ B.invisibility = 0
+
+/proc/turf_hasblood(var/turf/test)
+ for (var/obj/effect/decal/cleanable/blood/b in test)
+ return 1
+ return 0
+
+#undef PPM
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 6d352dd264f..68e097ceea5 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -134,6 +134,20 @@
to_chat(src, span("warning", "Your [stomach.name] is full!"))
return FALSE
+ if(species?.gluttonous & GLUT_MESSY)
+ if(ismob(victim))
+ var/mob/M = victim
+ if(ishuman(victim))
+ to_chat(src, span("warning", "You can't devour humanoids!"))
+ return FALSE
+ for(var/obj/item/grab/G in M.grabbed_by)
+ if(G && G.state < GRAB_NECK)
+ if(!silent)
+ to_chat(src, span("warning", "You need a tighter hold on \the [M]!"))
+ return FALSE
+ else
+ return FALSE
+
. = stomach.get_devour_time(victim) || ..()
/mob/living/carbon/human/get_ingested_reagents()
diff --git a/code/modules/mob/living/carbon/human/species/station/unathi/unathi.dm b/code/modules/mob/living/carbon/human/species/station/unathi/unathi.dm
index 77bab661890..01159cea24d 100644
--- a/code/modules/mob/living/carbon/human/species/station/unathi/unathi.dm
+++ b/code/modules/mob/living/carbon/human/species/station/unathi/unathi.dm
@@ -16,7 +16,8 @@
)
primitive_form = "Stok"
darksight = 3
- gluttonous = GLUT_SMALLER|GLUT_ITEM_TINY
+ gluttonous = GLUT_MESSY
+ stomach_capacity = 7
slowdown = 0.5
brute_mod = 0.8
diff --git a/code/modules/organs/internal/stomach.dm b/code/modules/organs/internal/stomach.dm
index 0dcfa6fa660..73962f35775 100644
--- a/code/modules/organs/internal/stomach.dm
+++ b/code/modules/organs/internal/stomach.dm
@@ -13,6 +13,9 @@
/obj/item/organ/internal/stomach/Destroy()
QDEL_NULL(ingested)
+ for(var/mob/M in contents)
+ if(M.stat != DEAD)
+ M.forceMove(owner.loc)
. = ..()
/obj/item/organ/internal/stomach/Initialize()
@@ -50,6 +53,8 @@
/obj/item/organ/internal/stomach/proc/is_full(var/atom/movable/food)
var/total = Floor(ingested.total_volume / 10)
+ if(species.gluttonous & GLUT_MESSY)
+ return FALSE //Don't need to check if the stomach is full if we're not using the contents.
for(var/a in contents + food)
if(ismob(a))
var/mob/M = a
@@ -68,7 +73,7 @@
var/mob/living/L = food
if((species.gluttonous & GLUT_TINY) && (L.mob_size <= MOB_TINY) && !ishuman(food)) // Anything MOB_TINY or smaller
return DEVOUR_SLOW
- else if((species.gluttonous & GLUT_SMALLER) && owner.mob_size > L.mob_size) // Anything we're larger than
+ else if((species.gluttonous & (GLUT_SMALLER|GLUT_MESSY)) && owner.mob_size > L.mob_size) // Anything we're larger than
return DEVOUR_SLOW
else if(species.gluttonous & GLUT_ANYTHING) // Eat anything ever
return DEVOUR_FAST
diff --git a/html/changelogs/mattatlas-ihatelore.yml b/html/changelogs/mattatlas-ihatelore.yml
new file mode 100644
index 00000000000..072e0a01349
--- /dev/null
+++ b/html/changelogs/mattatlas-ihatelore.yml
@@ -0,0 +1,43 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+# balance
+# admin
+# backend
+# security
+# refactor
+#################################
+
+# Your name.
+author: MattAtlas
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - rscdel: "Unathi can no longer swallow mobs or items whole."
+ - rscadd: "Instead, they now have their old devour, still tied to a grab. You need a red grab now, however. It does not work on humans."
+ - tweak: "Unathi devour is now overall faster."