diff --git a/code/game/objects/structures/animal_den.dm b/code/game/objects/structures/animal_den.dm
new file mode 100644
index 0000000000..33f50297a0
--- /dev/null
+++ b/code/game/objects/structures/animal_den.dm
@@ -0,0 +1,161 @@
+/obj/structure/animal_den
+ name = "den"
+ icon = 'icons/obj/structures/den.dmi'
+ icon_state = "den1"
+ desc = "A rough-walled, shallow den dug into the earth."
+ density = FALSE
+ opacity = FALSE
+
+/obj/structure/animal_den/Initialize()
+ icon_state = "den[rand(1,3)]"
+ . = ..()
+
+/obj/structure/animal_den/proc/remove_from_den(var/atom/movable/AM)
+ AM.dropInto(get_turf(src))
+ if(ismob(AM))
+ var/mob/M = AM
+ if(M.client)
+ M.client.eye = M.client.mob
+ M.client.perspective = MOB_PERSPECTIVE
+ update_icon()
+
+/obj/structure/animal_den/relaymove(mob/user, direction)
+ remove_from_den(user)
+ user.visible_message(SPAN_NOTICE("\The [user] emerges from \the [src]."))
+
+/obj/structure/animal_den/Destroy()
+ for(var/atom/movable/AM in contents)
+ remove_from_den(AM)
+ . = ..()
+
+/obj/structure/animal_den/attack_hand(mob/user)
+ if(try_grab_occupant(user))
+ return TRUE
+ . = ..()
+
+/obj/structure/animal_den/attackby(obj/item/O, mob/user)
+ if(O.has_tool_quality(TOOL_SHOVEL) && try_collapse_den(user))
+ return TRUE
+ . = ..()
+
+/obj/structure/animal_den/proc/try_grab_occupant(var/mob/user)
+ if(user.a_intent != I_GRAB)
+ return FALSE
+ if(!do_after(user, 1 SECOND, src) || QDELETED(src))
+ return TRUE
+ if(!length(contents))
+ to_chat(user, SPAN_WARNING("There is nothing inside \the [src]."))
+ return TRUE
+ var/atom/movable/AM = pick(contents)
+ remove_from_den(AM)
+ user.visible_message(SPAN_NOTICE("\The [user] reaches into \the [src] and pulls out \the [AM]!"))
+ update_icon()
+ return TRUE
+
+/obj/structure/animal_den/proc/try_collapse_den(var/mob/user)
+ if(user.a_intent != I_HURT)
+ return FALSE
+ // Humans can only get here via a shovel, so it's fine for them to be small.
+ if(user.mob_size < MOB_MEDIUM && !ishuman(user))
+ to_chat(user, SPAN_WARNING("You are too small to collapse \the [src]."))
+ return TRUE
+ if(!do_after(user, 5 SECONDS, src) || QDELETED(src))
+ return TRUE
+ if(length(contents))
+ to_chat(user, SPAN_WARNING("You can't collapse \the [src]; it's occupied."))
+ else
+ to_chat(user, SPAN_NOTICE("You collapse \the [src]."))
+ playsound(user, 'sound/weapons/thudswoosh.ogg', 50)
+ qdel(src)
+ return TRUE
+
+/obj/structure/animal_den/attack_generic(mob/user, damage, attack_verb)
+ // Animal interactions with their nests.
+ if(istype(user, /mob/living/simple_mob/animal))
+ if(try_collapse_den(user))
+ return TRUE
+ if(try_grab_occupant(user))
+ return TRUE
+ if(!do_after(user, src, 1 SECOND))
+ return TRUE
+ if(length(contents))
+ to_chat(user, SPAN_WARNING("You can't hide in \the [src]; it's occupied."))
+ return TRUE
+ user.visible_message(SPAN_NOTICE("\The [user] disappears into \the [src]."))
+ user.forceMove(src)
+ update_icon()
+ return TRUE
+ return ..()
+
+/mob/living/simple_mob/animal/proc/get_eye_color()
+ return COLOR_WHITE
+
+/obj/structure/animal_den/update_icon()
+ cut_overlays()
+ for(var/mob/living/simple_mob/animal/critter in contents)
+ if(critter.stat == CONSCIOUS && !critter.resting)
+ var/image/I = image(icon, "eyes[rand(1,3)]")
+ I.color = critter.get_eye_color()
+ I.pixel_x = rand(-3, 3)
+ I.pixel_y = rand(-3, 3)
+ add_overlay(I)
+
+// Joinable den for critter spawns with ghost players.
+/obj/structure/animal_den/ghost_join
+ var/mob/living/critter
+ var/ban_check = "Critter"
+ var/welcome_text
+
+/obj/structure/animal_den/ghost_join/Initialize()
+ . = ..()
+ name = "den" // to remove mapping identifiers.
+ if(ispath(critter))
+ critter = new critter(src)
+
+/obj/structure/animal_den/ghost_join/remove_from_den(var/atom/movable/AM)
+ . = ..()
+ if(critter == AM && critter.loc != src)
+ if(critter.resting)
+ critter.lay_down() // you woke them up :(
+ critter = null
+
+/obj/structure/animal_den/ghost_join/examine(mob/user, infix, suffix)
+ . = ..()
+ if(isobserver(user))
+ if(critter)
+ if(ban_check && ckey_is_jobbanned(user.ckey, ban_check))
+ to_chat(user, SPAN_WARNING("You are banned from [ban_check] roles and cannot join via this den."))
+ else if(user.MayRespawn(TRUE))
+ to_chat(user, SPAN_NOTICE("Click on the den to join as \a [critter]."))
+ else
+ to_chat(user, SPAN_WARNING("This den is no longer available for joining."))
+
+/obj/structure/animal_den/ghost_join/attack_ghost(mob/user)
+ if(critter)
+ transfer_personality(user)
+ return
+ return ..()
+
+/obj/structure/animal_den/ghost_join/proc/transfer_personality(var/mob/user)
+ set waitfor = FALSE
+ if(!critter)
+ return
+ if(user.mind)
+ user.mind.transfer_to(critter)
+ else
+ critter.key = user.key
+
+ var/mob/living/critter_ref = critter
+ critter = null
+
+ // Sleep long enough for them to login and get any other text out of the way.
+ sleep(1)
+ to_chat(critter_ref, SPAN_NOTICE("You are \a [critter]!"))
+ if(welcome_text)
+ to_chat(critter_ref, SPAN_NOTICE(welcome_text))
+
+ // Sleep long enough for the logged-in critter to update state and regen icon.
+ sleep(SSmobs.wait)
+ if(critter_ref.resting) // get up lazybones
+ critter_ref.lay_down()
+ update_icon()
diff --git a/code/game/objects/structures/flora/flora.dm b/code/game/objects/structures/flora/flora.dm
index a39370bb7b..ddf70cd639 100644
--- a/code/game/objects/structures/flora/flora.dm
+++ b/code/game/objects/structures/flora/flora.dm
@@ -71,9 +71,9 @@
..(W, user)
-/obj/structure/flora/proc/can_harvest(var/obj/item/I)
+/obj/structure/flora/proc/can_harvest(var/obj/item/I, var/ignore_tool = FALSE)
. = FALSE
- if(harvest_tool && istype(I, harvest_tool) && harvest_loot && harvest_loot.len && harvest_count < max_harvests)
+ if((ignore_tool || (harvest_tool && istype(I, harvest_tool))) && harvest_loot && harvest_loot.len && harvest_count < max_harvests)
. = TRUE
return .
@@ -89,6 +89,33 @@
harvest_count++
return AM
+/obj/structure/flora/attack_generic(mob/user, damage, attack_verb)
+ if(istype(user, /mob/living/simple_mob/animal) && user.a_intent != I_HURT)
+ var/mob/living/simple_mob/animal/critter = user
+ if(!critter.forager)
+ return ..()
+ if(!can_forage(critter))
+ to_chat(critter, SPAN_WARNING("You cannot see any edible fruit on \the [src]."))
+ return TRUE
+ show_animal_foraging_message(critter)
+ if(!do_after(critter, 2 SECONDS, src) || QDELETED(src) || !can_forage(user))
+ return TRUE
+ show_animal_eating_message(critter)
+ playsound(critter, 'sound/items/eatfood.ogg', rand(10,50), 1)
+ harvest_count++
+ critter.eat_food_item(spawn_harvest(pickweight(harvest_loot)))
+ return TRUE
+ return ..()
+
+/obj/structure/flora/proc/can_forage(var/mob/critter)
+ return can_harvest(ignore_tool = TRUE)
+
+/obj/structure/flora/proc/show_animal_foraging_message(var/mob/critter)
+ critter.visible_message(SPAN_NOTICE("\The [critter] begins sniffing at \the [src]."))
+
+/obj/structure/flora/proc/show_animal_eating_message(var/mob/critter)
+ critter.visible_message(SPAN_NOTICE("\The [critter] rips away some of the fruit from \the [src]."))
+
//bushes
/obj/structure/flora/bush
name = "bush"
diff --git a/code/game/objects/structures/flora/trees.dm b/code/game/objects/structures/flora/trees.dm
index 6b8ca8c5d1..b7d70d64c6 100644
--- a/code/game/objects/structures/flora/trees.dm
+++ b/code/game/objects/structures/flora/trees.dm
@@ -30,9 +30,9 @@
/obj/structure/flora/tree/proc/choose_icon_state()
return icon_state
-/obj/structure/flora/tree/can_harvest(var/obj/item/I)
+/obj/structure/flora/tree/can_harvest(var/obj/item/I, var/ignore_tool = FALSE)
. = FALSE
- if(!is_stump && harvest_tool && istype(I, harvest_tool) && harvest_loot && harvest_loot.len && harvest_count < max_harvests)
+ if(!is_stump && (ignore_tool || (harvest_tool && istype(I, harvest_tool))) && harvest_loot && harvest_loot.len && harvest_count < max_harvests)
. = TRUE
return .
@@ -139,6 +139,12 @@
return results
+/obj/structure/flora/tree/show_animal_foraging_message(var/mob/critter)
+ critter.visible_message(SPAN_NOTICE("\The [critter] begins searching through the foliage of \the [src]."))
+
+/obj/structure/flora/tree/show_animal_eating_message(var/mob/critter)
+ critter.visible_message(SPAN_NOTICE("\The [critter] pulls down some low-hanging fruit from \the [src]."))
+
// Subtypes.
// Pine trees
diff --git a/code/game/turfs/simulated/outdoors/outdoors.dm b/code/game/turfs/simulated/outdoors/outdoors.dm
index 39bd4ca1b2..61eafef4d3 100644
--- a/code/game/turfs/simulated/outdoors/outdoors.dm
+++ b/code/game/turfs/simulated/outdoors/outdoors.dm
@@ -8,7 +8,7 @@ var/global/list/turf_edge_cache = list()
var/outdoors = OUTDOORS_AREA
/area
- // If a turf's `outdoors` variable is set to `OUTDOORS_AREA`,
+ // If a turf's `outdoors` variable is set to `OUTDOORS_AREA`,
// it will decide if it's outdoors or not when being initialized based on this var.
var/outdoors = OUTDOORS_NO
@@ -39,6 +39,30 @@ var/global/list/turf_edge_cache = list()
if(can_dig && prob(33))
loot_count = rand(1,3)
+/turf/simulated/floor/outdoors/attack_generic(mob/user)
+ if(isanimal(user) && user.loc == src && user.a_intent == I_HELP)
+ var/mob/living/simple_mob/animal/critter = user
+ if(critter.burrower)
+ if(locate(/obj/structure/animal_den) in contents)
+ to_chat(critter, SPAN_WARNING("There is already a den here."))
+ return TRUE
+ critter.visible_message(SPAN_NOTICE("\The [critter] begins digging industriously."))
+ if(!do_after(critter, 10 SECONDS, src))
+ return TRUE
+ if(locate(/obj/structure/animal_den) in contents)
+ to_chat(critter, SPAN_WARNING("There is already a den here."))
+ return TRUE
+ critter.visible_message(SPAN_NOTICE("\The [critter] finishes digging a den!"))
+ new /obj/structure/animal_den(src)
+ if(prob(66))
+ var/worms = 0
+ for(var/worm in 1 to rand(3))
+ new /obj/item/reagent_containers/food/snacks/worm(src)
+ worms++
+ to_chat(critter, SPAN_NOTICE("You unearthed [worms] worm\s!"))
+ return TRUE
+ . = ..()
+
/turf/simulated/floor/outdoors/attackby(obj/item/C, mob/user)
if(can_dig && istype(C, /obj/item/shovel))
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index f4b522598e..29b59d4aed 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1640,4 +1640,25 @@
/mob/living/carbon/human/get_sound_volume_multiplier()
. = ..()
for(var/obj/item/clothing/C in list(l_ear, r_ear, head))
- . = min(., C.volume_multiplier)
\ No newline at end of file
+ . = min(., C.volume_multiplier)
+
+/mob/living/carbon/human/rip_out_internal_organ(var/zone, var/skip_wounding = FALSE, var/damage_descriptor)
+
+ var/obj/item/organ/organ
+ var/obj/item/organ/external/limb
+ if(zone)
+ limb = organs_by_name[zone]
+ if(limb && length(limb.internal_organs))
+ organ = pick_n_take(limb.internal_organs)
+ if(!organ)
+ organ = ..(skip_wounding = TRUE)
+
+ if(organ)
+ organ.removed()
+ if(organ.parent_organ && !limb)
+ limb = organs_by_name[organ.parent_organ]
+ if(limb)
+ limb.take_damage(rand(10,20), 0, TRUE, TRUE, damage_descriptor)
+ else
+ take_damage(rand(10, 20))
+ return organ
diff --git a/code/modules/mob/living/carbon/human/species/species_attack.dm b/code/modules/mob/living/carbon/human/species/species_attack.dm
index 9be2a2f7e8..baa083da81 100644
--- a/code/modules/mob/living/carbon/human/species/species_attack.dm
+++ b/code/modules/mob/living/carbon/human/species/species_attack.dm
@@ -99,4 +99,4 @@
/datum/unarmed_attack/stomp/weak/show_attack(var/mob/living/carbon/human/user, var/mob/living/carbon/human/target, var/zone, var/attack_damage)
var/obj/item/organ/external/affecting = target.get_organ(zone)
user.visible_message("[user] jumped up and down on \the [target]'s [affecting.name]!")
- playsound(user, attack_sound, 25, 1, -1)
\ No newline at end of file
+ playsound(user, attack_sound, 25, 1, -1)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index d01f01e461..0188488c7f 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1143,7 +1143,7 @@
var/turf/T = get_turf(src)
var/obj/effect/overmap/visitable/M = get_overmap_sector(T.z)
-
+
if(istype(M))
if(M.in_space)
if(T.z in using_map.station_levels)
@@ -1156,3 +1156,10 @@
var/datum/map_z_level/zlevel = using_map.zlevels["[T.z]"]
if(istype(zlevel))
. |= zlevel.event_regions
+
+// kali maaaaa
+/mob/living/proc/rip_out_internal_organ(var/zone, var/skip_wounding = FALSE, var/damage_descriptor)
+ if(length(internal_organs))
+ . = pick_n_take(internal_organs)
+ if(!skip_wounding)
+ take_damage(rand(10,20))
diff --git a/code/modules/mob/living/simple_mob/on_click.dm b/code/modules/mob/living/simple_mob/on_click.dm
index 4b824c9580..1434373b80 100644
--- a/code/modules/mob/living/simple_mob/on_click.dm
+++ b/code/modules/mob/living/simple_mob/on_click.dm
@@ -11,32 +11,7 @@
var/obj/O = A
return O.attack_hand(src)
- switch(a_intent)
- if(I_HELP)
- if(isliving(A))
- custom_emote(1,"[pick(friendly)] \the [A]!")
-
- if(I_HURT)
- if(can_special_attack(A) && special_attack_target(A))
- return
-
- else if(melee_damage_upper == 0 && istype(A,/mob/living))
- custom_emote(1,"[pick(friendly)] \the [A]!")
-
- else
- attack_target(A)
-
- if(I_GRAB)
- if(has_hands)
- A.attack_hand(src)
- else
- attack_target(A)
-
- if(I_DISARM)
- if(has_hands)
- A.attack_hand(src)
- else
- attack_target(A)
+ return do_interaction(A)
/mob/living/simple_mob/RangedAttack(var/atom/A)
// setClickCooldown(get_attack_speed())
@@ -45,4 +20,40 @@
return
if(projectiletype)
- shoot_target(A)
\ No newline at end of file
+ shoot_target(A)
+
+/mob/living/simple_mob/proc/do_interaction(var/atom/A)
+ switch(a_intent)
+ if(I_HURT)
+ return do_hurt_interaction(A)
+ if(I_GRAB)
+ return do_grab_interaction(A)
+ if(I_DISARM)
+ return do_disarm_interaction(A)
+ return do_help_interaction(A)
+
+/mob/living/simple_mob/proc/do_help_interaction(var/atom/A)
+ if(isliving(A))
+ custom_emote(1,"[pick(friendly)] \the [A]!")
+ return TRUE
+ return FALSE
+
+/mob/living/simple_mob/proc/do_hurt_interaction(var/atom/A)
+ if(can_special_attack(A) && special_attack_target(A))
+ return
+ else if(melee_damage_upper == 0 && istype(A,/mob/living))
+ custom_emote(1,"[pick(friendly)] \the [A]!")
+ else
+ attack_target(A)
+
+/mob/living/simple_mob/proc/do_disarm_interaction(var/atom/A)
+ if(has_hands)
+ A.attack_hand(src)
+ else
+ attack_target(A)
+
+/mob/living/simple_mob/proc/do_grab_interaction(var/atom/A)
+ if(has_hands)
+ A.attack_hand(src)
+ else
+ attack_target(A)
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm
index fa3f58e219..8200f5f87a 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm
@@ -23,5 +23,44 @@
/obj/item/stack/animalhide = 3\
)
+ var/forager = TRUE // Can eat from trees and bushes.
+ var/scavenger = FALSE // Can eat from corpses.
+ var/burrower = FALSE // Can dig dens.
+
/decl/mob_organ_names/quadruped //Most subtypes have this basic body layout.
- hit_zones = list("head", "torso", "left foreleg", "right foreleg", "left hind leg", "right hind leg", "tail")
\ No newline at end of file
+ hit_zones = list("head", "torso", "left foreleg", "right foreleg", "left hind leg", "right hind leg", "tail")
+
+/mob/living/simple_mob/animal/do_interaction(var/atom/A)
+ var/static/list/atom_types_with_animal_interactions = list(
+ /obj/structure/animal_den,
+ /obj/structure/flora,
+ /turf/simulated/floor/outdoors
+ )
+ for(var/checktype in atom_types_with_animal_interactions)
+ if(istype(A, checktype))
+ attack_generic(A, 0, "investigates")
+ return TRUE
+ if(scavenger && isliving(A) && a_intent == I_HURT)
+ var/mob/living/M = A
+ if(M.stat == DEAD && length(M.internal_organs))
+ to_chat(src, SPAN_NOTICE("You dig into the guts of \the [M], hunting for the sweetest meat."))
+ if(do_after(src, M, 2 SECOND) && !QDELETED(M) && length(M.internal_organs))
+ var/obj/item/organ = M.rip_out_internal_organ(check_zone(zone_sel.selecting), damage_descriptor = "animal teeth")
+ if(organ)
+ visible_message(SPAN_DANGER("\The [src] rips \the [organ] out of \the [M] and devours [organ.gender == PLURAL ? "them" : "it"]!"))
+ eat_food_item(organ)
+ if(!length(M.internal_organs) && M.gib_on_butchery)
+ M.gib()
+ return TRUE
+ return ..()
+
+/mob/living/simple_mob/animal/proc/eat_food_item(var/obj/item/snack, var/silent)
+ if(istype(snack, /obj/item/organ))
+ var/obj/item/organ/organ = snack
+ if(organ.meat_type)
+ eat_food_item(new organ.meat_type(src), silent = TRUE)
+ if(!silent)
+ playsound(src, 'sound/items/eatfood.ogg', rand(10,50), 1)
+ if(snack.reagents?.total_volume)
+ snack.reagents.trans_to_mob(src, snack.reagents.total_volume, CHEM_INGEST)
+ qdel(snack)
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm
index 03a253d0b5..c0d3ad81d9 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/kururak.dm
@@ -425,4 +425,4 @@
attack_speed_percent = 0.8
/decl/mob_organ_names/kururak
- hit_zones = list("head", "chest", "left foreleg", "right foreleg", "left hind leg", "right hind leg", "far left tail", "far right tail", "left middle tail", "right middle tail")
\ No newline at end of file
+ hit_zones = list("head", "chest", "left foreleg", "right foreleg", "left hind leg", "right hind leg", "far left tail", "far right tail", "left middle tail", "right middle tail")
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/siffet.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/siffet.dm
index 40ae65d3bc..1e7a63f417 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/sif/siffet.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/siffet.dm
@@ -40,6 +40,7 @@
say_list_type = /datum/say_list/siffet
ai_holder_type = /datum/ai_holder/simple_mob/siffet
+ burrower = TRUE
/datum/say_list/siffet
speak = list("Yap!", "Heh!", "Huff.")
@@ -58,4 +59,8 @@
/mob/living/simple_mob/animal/sif/siffet/IIsAlly(mob/living/L)
. = ..()
if(!. && L.mob_size > 10) //Attacks things it considers small enough to take on, otherwise only attacks if attacked.
- return TRUE
\ No newline at end of file
+ return TRUE
+
+/obj/structure/animal_den/ghost_join/siffet
+ name = "siffet den"
+ critter = /mob/living/simple_mob/animal/sif/siffet
diff --git a/icons/mob/64x32.dmi b/icons/mob/64x32.dmi
index 615fd87a01..b40f672c2d 100644
Binary files a/icons/mob/64x32.dmi and b/icons/mob/64x32.dmi differ
diff --git a/icons/obj/structures/den.dmi b/icons/obj/structures/den.dmi
new file mode 100644
index 0000000000..8fe7d5ccc7
Binary files /dev/null and b/icons/obj/structures/den.dmi differ
diff --git a/polaris.dme b/polaris.dme
index 7325f79a9c..db9da46c5c 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1178,6 +1178,7 @@
#include "code\game\objects\random\mob.dm"
#include "code\game\objects\random\spacesuits.dm"
#include "code\game\objects\random\unidentified\medicine.dm"
+#include "code\game\objects\structures\animal_den.dm"
#include "code\game\objects\structures\barricades.dm"
#include "code\game\objects\structures\barsign.dm"
#include "code\game\objects\structures\bedsheet_bin.dm"