diff --git a/code/datums/components/crafting/recipes/recipes_misc.dm b/code/datums/components/crafting/recipes/recipes_misc.dm
index 86d1e77661..214c1b1427 100644
--- a/code/datums/components/crafting/recipes/recipes_misc.dm
+++ b/code/datums/components/crafting/recipes/recipes_misc.dm
@@ -324,6 +324,13 @@
result = /obj/item/toy/sword/cx
subcategory = CAT_MISCELLANEOUS
category = CAT_MISC
+
+/datum/crafting_recipe/catgirlplushie
+ name = "Catgirl Plushie"
+ reqs = list(/obj/item/toy/plush/hairball = 3)
+ result = /obj/item/toy/plush/catgirl
+ subcategory = CAT_MISCELLANEOUS
+ category = CAT_MISC
////////////
//Unsorted//
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index 95177f6869..8f98917ed3 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -196,3 +196,7 @@
description = "That work of art was so great it made me believe in the goodness of humanity. Says a lot in a place like this.\n"
mood_change = 4
timeout = 4 MINUTES
+
+/datum/mood_event/cleared_stomach
+ description = "Feels nice to get that out of the way!\n"
+ mood_change = 3
diff --git a/code/datums/wounds/cuts.dm b/code/datums/wounds/cuts.dm
index 0ab3bbd5f8..59ce286884 100644
--- a/code/datums/wounds/cuts.dm
+++ b/code/datums/wounds/cuts.dm
@@ -152,6 +152,11 @@
user.visible_message("[user] licks the wounds on [victim]'s [limb.name].", "You lick some of the wounds on [victim]'s [limb.name]", ignored_mobs=victim)
to_chat(victim, "[user] licks the wounds on your [limb.name]! minimum_flow)
try_handling(user)
diff --git a/code/game/objects/items/plushes.dm b/code/game/objects/items/plushes.dm
index 00a9ff22f4..44205f4884 100644
--- a/code/game/objects/items/plushes.dm
+++ b/code/game/objects/items/plushes.dm
@@ -8,6 +8,7 @@
resistance_flags = FLAMMABLE
var/list/squeak_override //Weighted list; If you want your plush to have different squeak sounds use this
var/stuffed = TRUE //If the plushie has stuffing in it
+ var/unstuffable = FALSE //for plushies that can't be stuffed
var/obj/item/grenade/grenade //You can remove the stuffing from a plushie and add a grenade to it for *nefarious uses*
//--love ~<3--
gender = NEUTER
@@ -174,6 +175,9 @@
/obj/item/toy/plush/attackby(obj/item/I, mob/living/user, params)
if(I.get_sharpness())
if(!grenade)
+ if(unstuffable)
+ to_chat(user, "Nothing to do here.")
+ return
if(!stuffed)
to_chat(user, "You already murdered it!")
return
@@ -187,6 +191,13 @@
grenade = null
return
if(istype(I, /obj/item/grenade))
+ if(unstuffable)
+ to_chat(user, "No... you should destroy it now!")
+ sleep(10)
+ if(QDELETED(user) || QDELETED(src))
+ return
+ SEND_SOUND(user, 'sound/weapons/armbomb.ogg')
+ return
if(stuffed)
to_chat(user, "You need to remove some stuffing first!")
return
@@ -743,3 +754,14 @@ GLOBAL_LIST_INIT(valid_plushie_paths, valid_plushie_paths())
attack_verb = list("headbutt", "scritched", "bit")
squeak_override = list('modular_citadel/sound/voice/nya.ogg' = 1)
can_random_spawn = FALSE
+
+
+/obj/item/toy/plush/hairball
+ name = "Hairball"
+ desc = "A bundle of undigested fibers and scales. Yuck."
+ icon_state = "Hairball"
+ unstuffable = TRUE
+ young = TRUE // Your own mouth-baby.
+ squeak_override = list('sound/misc/splort.ogg'=1)
+ attack_verb = list("sploshed", "splorted", "slushed")
+ can_random_spawn = FALSE
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 5c5821381e..99e1f1481b 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -2336,3 +2336,53 @@
reagent_state = SOLID
color = "#E6E6DA"
taste_mult = 0
+
+
+/datum/reagent/hairball
+ name = "Hairball"
+ description = "A bundle of keratinous bits and fibers, not easily digestible."
+ reagent_state = SOLID
+ can_synth = FALSE
+ metabolization_rate = 0.05 * REAGENTS_METABOLISM
+ taste_description = "wet hair"
+ var/amount = 0
+ var/knotted = FALSE
+
+/datum/reagent/hairball/on_mob_life(mob/living/carbon/M)
+ amount = M.reagents.get_reagent_amount(/datum/reagent/hairball)
+
+ if(amount < 10)
+ if(prob(10))
+ M.losebreath += 1
+ M.emote("cough")
+ to_chat(M, "You clear your throat.")
+ else
+ if(!knotted)
+ to_chat(M, "You feel a knot in your stomach.")
+ knotted = TRUE
+
+ if(prob(5 + amount * 0.5)) // don't want this to cause too much damage
+ M.losebreath += 2
+ to_chat(M, "You feel a knot in your throat.")
+ M.emote("cough")
+
+ else if(prob(amount - 4))
+ to_chat(M, "Your stomach feels awfully bloated.")
+ playsound(M,'sound/voice/catpeople/distressed.ogg', 50, FALSE)
+ M.visible_message("[M] seems distressed!.", ignored_mobs=M)
+
+ else if(prob(amount - 8))
+ knotted = FALSE
+ playsound(M,'sound/voice/catpeople/puking.ogg', 110, FALSE)
+ M.Immobilize(30)
+ sleep(30) //snowflake but it works, don't wanna proc this
+ if(QDELETED(M) || QDELETED(src)) //this handles race conditions about m or src not existing.
+ return
+ M.visible_message("[M] throws up a hairball! Disgusting!", ignored_mobs=M)
+ new /obj/item/toy/plush/hairball(get_turf(M))
+ to_chat(M, "Aaaah that's better!")
+ SEND_SIGNAL(M, COMSIG_ADD_MOOD_EVENT, "cleared_stomach", /datum/mood_event/cleared_stomach, name)
+ M.reagents.del_reagent(/datum/reagent/hairball)
+ return
+ ..()
+
diff --git a/icons/obj/plushes.dmi b/icons/obj/plushes.dmi
index 3abb25d8b2..11d02a46cc 100644
Binary files a/icons/obj/plushes.dmi and b/icons/obj/plushes.dmi differ
diff --git a/sound/voice/catpeople/distressed.ogg b/sound/voice/catpeople/distressed.ogg
new file mode 100644
index 0000000000..cebe73dffc
Binary files /dev/null and b/sound/voice/catpeople/distressed.ogg differ
diff --git a/sound/voice/catpeople/license.txt b/sound/voice/catpeople/license.txt
new file mode 100644
index 0000000000..7218480ddb
--- /dev/null
+++ b/sound/voice/catpeople/license.txt
@@ -0,0 +1,2 @@
+distressed_cat.ogg from Cat annoyed meow / wail by jbierfeldt at https://freesound.org/people/jbierfeldt/sounds/440735/, chopped up and ogged
+cat_puking.ogg from catpuking mp3 by NoiseCollector and Mocha the cat at https://freesound.org/people/NoiseCollector/sounds/80778/, chopped up, volume altered and ogged
\ No newline at end of file
diff --git a/sound/voice/catpeople/puking.ogg b/sound/voice/catpeople/puking.ogg
new file mode 100644
index 0000000000..e19c10858f
Binary files /dev/null and b/sound/voice/catpeople/puking.ogg differ