diff --git a/code/datums/ai/_ai.dm b/code/datums/ai/_ai.dm
index 395d2e9df5d..be51dd0ee08 100644
--- a/code/datums/ai/_ai.dm
+++ b/code/datums/ai/_ai.dm
@@ -257,6 +257,11 @@
/datum/mob_controller/proc/check_memory(mob/speaker, message)
return FALSE
+/// General-purpose scooping reaction proc, used by /passive.
+/// Returns TRUE if the scoop should proceed, FALSE if it should be canceled.
+/datum/mob_controller/proc/scooped_by(mob/initiator)
+ return TRUE
+
// Enemy tracking - used on /aggressive
/datum/mob_controller/proc/get_enemies()
return _enemies
diff --git a/code/datums/ai/passive.dm b/code/datums/ai/passive.dm
index 5d54d2cbf8a..73d29d470b6 100644
--- a/code/datums/ai/passive.dm
+++ b/code/datums/ai/passive.dm
@@ -56,3 +56,20 @@
if(istype(source))
flee_target = weakref(source)
update_targets()
+
+/datum/mob_controller/passive
+ var/decl/skill/scooping_skill // If overridden (on a subtype, on a map/downstream, etc) check this skill to see if scooping should succeed uncontested.
+ var/scooping_skill_req = SKILL_ADEPT
+
+/datum/mob_controller/passive/scooped_by(mob/living/initiator)
+ if(is_friend(initiator))
+ return TRUE
+ if(is_enemy(initiator) || (scooping_skill && initiator.skill_fail_prob(scooping_skill, 50, scooping_skill_req))) // scary, try to wriggle away
+ retaliate(initiator) // run! run like the wind!
+ if(!initiator.skill_fail_prob(SKILL_HAULING, 100, SKILL_EXPERT))
+ to_chat(initiator, SPAN_WARNING("\The [body] tries to wriggle out of your grasp, but you hold on tight!"))
+ return TRUE
+ to_chat(initiator, SPAN_WARNING("\The [body] wriggles out of your grasp!"))
+ initiator.drop_from_inventory(body)
+ return FALSE
+ return TRUE
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
index 57f294e53af..ee8e609154b 100644
--- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
+++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
@@ -11,7 +11,10 @@
natural_weapon = /obj/item/natural_weapon/hooves
butchery_data = /decl/butchery_data/animal/ruminant/goat
ai = /datum/mob_controller/aggressive/goat
+ // TODO: milkable extension to combine goat/cow behavior?
var/datum/reagents/udder = null
+ var/decl/skill/milking_skill = SKILL_BOTANY
+ var/milking_skill_req = SKILL_BASIC
/datum/mob_controller/aggressive/goat
expected_type = /mob/living/simple_animal/hostile/goat
@@ -104,17 +107,37 @@
if((. = ..()) && stat == CONSCIOUS && udder && prob(5))
udder.add_reagent(/decl/material/liquid/drink/milk, rand(5, 10))
-/mob/living/simple_animal/hostile/goat/attackby(var/obj/item/O, var/mob/user)
- var/obj/item/chems/glass/G = O
- if(stat == CONSCIOUS && istype(G) && ATOM_IS_OPEN_CONTAINER(G))
- user.visible_message("[user] milks [src] using \the [O].")
- var/transfered = udder.trans_type_to(G, /decl/material/liquid/drink/milk, rand(5,10))
- if(G.reagents.total_volume >= G.volume)
- to_chat(user, "\The [O] is full.")
- if(!transfered)
- to_chat(user, "The udder is dry. Wait a bit longer...")
- else
- ..()
+/mob/living/simple_animal/hostile/goat/attackby(var/obj/item/used_item, var/mob/user)
+ var/obj/item/chems/container = used_item
+ if(stat == CONSCIOUS && istype(container) && ATOM_IS_OPEN_CONTAINER(container))
+ if(ai?.is_enemy(user))
+ if(user.skill_check(milking_skill, SKILL_PROF))
+ to_chat(user, SPAN_NOTICE("\The [src] goes still at your touch."))
+ ai.remove_enemy(user)
+ stop_automove()
+ else
+ to_chat(user, SPAN_DANGER("You can't milk \the [src] while it's trying to attack you!"))
+ return TRUE
+ if(container.reagents.total_volume >= container.volume)
+ to_chat(user, SPAN_WARNING("\The [container] is full."))
+ return TRUE
+ // Goats REALLY don't like being milked if you're unskilled.
+ if(user.skill_fail_prob(milking_skill, 40, milking_skill_req))
+ ai?.retaliate()
+ return TRUE
+ if(!udder.total_volume)
+ to_chat(user, SPAN_WARNING("The udder is dry. Wait a bit longer."))
+ return TRUE
+ user.visible_message(SPAN_NOTICE("\The [user] starts milking \the [src] into \the [container]."), SPAN_NOTICE("You start milking \the [src] into \the [container]."))
+ if(!user.do_skilled(milking_skill, 4 SECONDS, milking_skill_req))
+ user.visible_message(SPAN_NOTICE("\The [user] stops milking \the [src]."), SPAN_NOTICE("You stop milking \the [src]."))
+ return TRUE
+ user.visible_message(SPAN_NOTICE("\The [user] milks \the [src] into \the [container]."), SPAN_NOTICE("You milk \the [src] into \the [container]."))
+ udder.trans_type_to(container, /decl/material/liquid/drink/milk, rand(5,10))
+ if(container.reagents.total_volume >= container.volume)
+ to_chat(user, SPAN_NOTICE("\The [container] is full."))
+ return TRUE
+ . = ..()
//cow
/mob/living/simple_animal/cow
@@ -134,6 +157,9 @@
"looks at you with a resigned expression",
"seems resigned to its fate"
)
+ var/decl/skill/milking_skill = SKILL_BOTANY
+ var/milking_skill_req = SKILL_BASIC
+ var/impatience = 0 // if you fail to milk it, this goes up. if it gets too high it'll flee
/datum/mob_controller/cow
emote_speech = list("moo?","moo","MOOOOOO")
@@ -150,25 +176,68 @@
QDEL_NULL(udder)
. = ..()
-/mob/living/simple_animal/cow/attackby(var/obj/item/O, var/mob/user)
- var/obj/item/chems/glass/G = O
- if(stat == CONSCIOUS && istype(G) && ATOM_IS_OPEN_CONTAINER(G))
- if(G.reagents.total_volume >= G.volume)
- to_chat(user, SPAN_WARNING("\The [O] is full."))
+/mob/living/simple_animal/cow/attackby(var/obj/item/used_item, var/mob/user)
+ var/obj/item/chems/container = used_item
+ if(stat == CONSCIOUS && istype(container) && ATOM_IS_OPEN_CONTAINER(container))
+ if(get_automove_target())
+ if(user.skill_check(milking_skill, SKILL_PROF))
+ to_chat(user, SPAN_NOTICE("\The [src] goes still at your touch."))
+ stop_automove()
+ else
+ to_chat(user, SPAN_WARNING("Wait for \the [src] to stop moving before you try milking it."))
+ return TRUE
+ if(container.reagents.total_volume >= container.volume)
+ to_chat(user, SPAN_WARNING("\The [container] is full."))
+ return TRUE
+ // Cows don't like being milked if you're unskilled.
+ if(user.skill_fail_prob(milking_skill, 40, milking_skill_req))
+ if(impatience > 3)
+ visible_message(SPAN_WARNING("\The [src] bellows and flees from \the [user]!"))
+ flee(user, upset = TRUE)
+ else
+ visible_message(SPAN_WARNING("\The [src] huffs and moves away from \the [user]."))
+ flee(user, upset = FALSE)
+ impatience++
return TRUE
if(!udder.total_volume)
to_chat(user, SPAN_WARNING("The udder is dry. Wait a bit longer."))
return TRUE
- user.visible_message(SPAN_NOTICE("\The [user] milks \the [src] using \the [O]."))
- udder.trans_type_to(G, /decl/material/liquid/drink/milk, rand(5,10))
- if(G.reagents.total_volume >= G.volume)
- to_chat(user, SPAN_NOTICE("\The [O] is full."))
+ user.visible_message(SPAN_NOTICE("\The [user] starts milking \the [src] into \the [container]."), SPAN_NOTICE("You start milking \the [src] into \the [container]."))
+ if(!user.do_skilled(milking_skill, 4 SECONDS, milking_skill_req))
+ user.visible_message(SPAN_NOTICE("\The [user] stops milking \the [src]."), SPAN_NOTICE("You stop milking \the [src]."))
+ return TRUE
+ user.visible_message(SPAN_NOTICE("\The [user] milks \the [src] into \the [container]."), SPAN_NOTICE("You milk \the [src] into \the [container]."))
+ udder.trans_type_to(container, /decl/material/liquid/drink/milk, rand(5,10))
+ if(container.reagents.total_volume >= container.volume)
+ to_chat(user, SPAN_NOTICE("\The [container] is full."))
return TRUE
. = ..()
+/mob/living/simple_animal/cow/proc/flee(atom/target, upset = FALSE)
+ var/static/datum/automove_metadata/_cow_flee_automove_metadata = new(
+ _move_delay = null,
+ _acceptable_distance = 7,
+ _avoid_target = TRUE
+ )
+ var/static/datum/automove_metadata/_cow_annoyed_automove_metadata = new(
+ _move_delay = null,
+ _acceptable_distance = 2,
+ _avoid_target = TRUE
+ )
+ if(upset)
+ set_moving_quickly()
+ else
+ set_moving_slowly()
+ start_automove(target, metadata = upset ? _cow_flee_automove_metadata : _cow_annoyed_automove_metadata)
+
/mob/living/simple_animal/cow/handle_living_non_stasis_processes()
- if((. = ..()) && udder && prob(5))
+ . = ..()
+ if(!.)
+ return
+ if(udder && prob(5))
udder.add_reagent(/decl/material/liquid/drink/milk, rand(5, 10))
+ if(!get_automove_target() && impatience > 0 && prob(10)) // if not fleeing, 10% chance to regain patience
+ impatience--
/mob/living/simple_animal/cow/default_disarm_interaction(mob/user)
if(stat != DEAD && !HAS_STATUS(src, STAT_WEAK))
@@ -194,6 +263,24 @@
ai = /datum/mob_controller/chick
holder_type = /obj/item/holder
var/amount_grown = 0
+ var/decl/skill/examine_skill = SKILL_BOTANY // for maps that change the default skills, or for alien eggs that need science/medical/anatomy instead
+ var/examine_difficulty = SKILL_ADEPT
+
+/mob/living/simple_animal/chick/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ if(!user.skill_check(examine_skill, examine_difficulty))
+ var/decl/skill/examine_skill_decl = GET_DECL(examine_skill)
+ to_chat(user, SPAN_SUBTLE("If you knew more about [lowertext(examine_skill_decl.name)], you could learn additional information about this."))
+ return
+ switch(amount_grown)
+ if(0 to 20)
+ to_chat(user, SPAN_NOTICE("It's still young."))
+ if(20 to 40)
+ to_chat(user, SPAN_NOTICE("It's starting to grow in its adult feathers."))
+ if(40 to 80)
+ to_chat(user, SPAN_NOTICE("It's grown in almost all its adult feathers."))
+ if(80 to 100)
+ to_chat(user, SPAN_NOTICE("It's almost fully grown."))
/datum/mob_controller/chick
emote_speech = list("Cherp.","Cherp?","Chirrup.","Cheep!")
@@ -209,7 +296,8 @@
/mob/living/simple_animal/chick/handle_living_non_stasis_processes()
if((. = ..()))
- amount_grown += rand(1,2)
+ if(prob(50)) // should take around 4 or 5 minutes to grow up, give or take
+ amount_grown += rand(1, 2)
if(amount_grown >= 100)
new /mob/living/simple_animal/fowl/chicken(src.loc)
qdel(src)
@@ -277,10 +365,10 @@ var/global/chicken_count = 0
if(istype(O, /obj/item/food)) //feedin' dem chickens
var/obj/item/food/G = O
if(findtext(G.get_grown_tag(), "wheat")) // includes chopped, crushed, dried etc.
- if(!stat && eggsleft < 8)
+ if(!stat && eggsleft < 4)
user.visible_message(SPAN_NOTICE("[user] feeds \the [O] to \the [src]! It clucks happily."), SPAN_NOTICE("You feed \the [O] to \the [src]! It clucks happily."), SPAN_NOTICE("You hear clucking."))
qdel(O)
- eggsleft += rand(1, 4)
+ eggsleft += rand(1, 2)
else
to_chat(user, SPAN_NOTICE("\The [src] doesn't seem hungry!"))
else
@@ -289,13 +377,13 @@ var/global/chicken_count = 0
..()
/mob/living/simple_animal/fowl/chicken/handle_living_non_stasis_processes()
- if((. = ..()) && prob(3) && eggsleft > 0)
+ if((. = ..()) && prob(1) && eggsleft > 0)
visible_message("[src] [pick("lays an egg.","squats down and croons.","begins making a huge racket.","begins clucking raucously.")]")
eggsleft--
var/obj/item/food/egg/E = new(get_turf(src))
E.pixel_x = rand(-6,6)
E.pixel_y = rand(-6,6)
- if(chicken_count < MAX_CHICKENS && prob(10))
+ if(chicken_count < MAX_CHICKENS && prob(30))
E.amount_grown = 1
START_PROCESSING(SSobj, E)
@@ -330,6 +418,46 @@ var/global/chicken_count = 0
/obj/item/food/egg
var/amount_grown = 0
+ var/decl/skill/examine_skill = SKILL_BOTANY // for maps that change the default skills, or for alien eggs that need science/medical/anatomy instead
+ var/examine_difficulty = SKILL_ADEPT
+
+/obj/item/food/egg/examine(mob/user, distance, infix, suffix)
+ . = ..()
+ if(isnull(examine_difficulty) || !ispath(examine_skill))
+ return
+ if(!user.skill_check(examine_skill, examine_difficulty))
+ var/decl/skill/examine_skill_decl = GET_DECL(examine_skill)
+ to_chat(user, SPAN_SUBTLE("If you knew more about [lowertext(examine_skill_decl.name)], you could learn additional information about this."))
+ return
+ if(distance > 1)
+ to_chat(user, SPAN_SUBTLE("You're too far away to learn anything about this."))
+ return
+ if(!user.get_held_slot_for_item(src))
+ to_chat(user, SPAN_NOTICE("You need to be holding \the [src] to examine it closer."))
+ return
+ // need a lit candle or lantern to check
+ var/too_hot = FALSE
+ var/obj/item/candle // not necessarily an actual candle, just a light source that won't fry the egg
+ for(var/obj/item/I in user.get_held_items())
+ if(I.light_power && I.light_range) // we have a light! todo: minimum power?
+ if(I.get_heat() >= /obj/item/flame/fuelled/lighter::lit_heat) // lighters are too hot!
+ too_hot = TRUE
+ candle = I
+ if(!too_hot)
+ break
+ if(too_hot)
+ to_chat(user, SPAN_WARNING("You can't use \the [candle] to examine \the [src], that would fry it!"))
+ return
+ else if(!candle)
+ to_chat(user, SPAN_NOTICE("You need to be holding a light source to examine this closer."))
+ return
+ switch(amount_grown)
+ if(0)
+ to_chat(user, SPAN_NOTICE("\The [src] is unfertilized."))
+ if(10 to 80)
+ to_chat(user, SPAN_NOTICE("There's something growing inside \the [src]."))
+ if(80 to 100)
+ to_chat(user, SPAN_NOTICE("\The [src] is about to hatch!"))
/obj/item/food/egg/Destroy()
if(amount_grown)
@@ -338,7 +466,8 @@ var/global/chicken_count = 0
/obj/item/food/egg/Process()
if(isturf(loc))
- amount_grown += rand(1,2)
+ if(prob(50))
+ amount_grown++
if(amount_grown >= 100)
visible_message("[src] hatches with a quiet cracking sound.")
new /mob/living/simple_animal/chick(get_turf(src))
diff --git a/code/modules/mob_holder/holder_mobs.dm b/code/modules/mob_holder/holder_mobs.dm
index d07a128335c..f2ba2cc988c 100644
--- a/code/modules/mob_holder/holder_mobs.dm
+++ b/code/modules/mob_holder/holder_mobs.dm
@@ -23,17 +23,20 @@
H.w_class = get_object_size()
if(initiator == src)
if(!target.equip_to_slot_if_possible(H, slot_back_str, del_on_fail=0, disable_warning=1))
- to_chat(initiator, "You can't climb onto [target]!")
- return
- to_chat(target, "\The [src] clambers onto you!")
- to_chat(initiator, "You climb up onto \the [target]!")
+ to_chat(initiator, SPAN_WARNING("You can't climb onto [target]!"))
+ return FALSE
+ to_chat(target, SPAN_NOTICE("\The [src] clambers onto you!"))
+ to_chat(initiator, SPAN_NOTICE("You climb up onto \the [target]!"))
else
if(!target.put_in_hands(H))
- to_chat(initiator, "Your hands are full!")
- return
+ to_chat(initiator, SPAN_WARNING("Your hands are full!"))
+ return FALSE
+
+ if(!ai?.scooped_by(initiator))
+ return FALSE // The AI canceled the scooping.
- to_chat(initiator, "You scoop up \the [src]!")
- to_chat(src, "\The [initiator] scoops you up!")
+ to_chat(initiator, SPAN_NOTICE("You scoop up \the [src]!"))
+ to_chat(src, SPAN_NOTICE("\The [initiator] scoops you up!"))
forceMove(H)
reset_offsets(0)
diff --git a/maps/shaded_hills/jobs/inn.dm b/maps/shaded_hills/jobs/inn.dm
index 11adfde1a96..5d3209ffebd 100644
--- a/maps/shaded_hills/jobs/inn.dm
+++ b/maps/shaded_hills/jobs/inn.dm
@@ -86,6 +86,7 @@
outfit_type = /decl/outfit/job/shaded_hills/farmer
min_skill = list(
SKILL_HAULING = SKILL_ADEPT, // farming can be demanding work
+ SKILL_HUSBANDRY = SKILL_ADEPT, // must be able to pick up and milk animals
SKILL_BOTANY = SKILL_ADEPT, // must be skilled enough to have plants reliably survive when planted
)
skill_points = 18
diff --git a/maps/shaded_hills/jobs/wilderness.dm b/maps/shaded_hills/jobs/wilderness.dm
index c1562cd9ca5..2ae6a03ef70 100644
--- a/maps/shaded_hills/jobs/wilderness.dm
+++ b/maps/shaded_hills/jobs/wilderness.dm
@@ -56,6 +56,7 @@
outfit_type = /decl/outfit/job/shaded_hills/forester
min_skill = list(
SKILL_HAULING = SKILL_ADEPT, // overall physical activity
+ SKILL_HUSBANDRY = SKILL_BASIC, // handling and caring for animals
SKILL_BOTANY = SKILL_BASIC, // growing and harvesting plants, trees, etc
SKILL_COOKING = SKILL_BASIC, // butchery
SKILL_CARPENTRY = SKILL_ADEPT, // tree felling
diff --git a/mods/content/fantasy/datum/skills.dm b/mods/content/fantasy/datum/skills.dm
index 38e64bbc83d..a2e6fa90f4b 100644
--- a/mods/content/fantasy/datum/skills.dm
+++ b/mods/content/fantasy/datum/skills.dm
@@ -5,6 +5,8 @@
#define SKILL_SCULPTING /decl/skill/crafting/sculpting
#define SKILL_ARTIFICE /decl/skill/crafting/artifice
+#define SKILL_HUSBANDRY /decl/skill/service/husbandry
+
/decl/skill/Initialize()
. = ..()
// Rename the default skill levels.
@@ -216,6 +218,23 @@
"Master" = "You're a specialized gardener. You can care for even the most exotic, fragile, or dangerous plants."
)
+/decl/skill/service/husbandry
+ name = "Animal Husbandry"
+ desc = "Your ability to raise and care for animals."
+ levels = list(
+ "Unskilled" = "You know next to nothing about animals. You can feed and clean up after them, but you know nothing about their biology, their behavior, or raising their young.",
+ "Basic" = "You've cared for farm animals before. You can care for the basic needs of an animal, and know how to do things like milk a cow or shear a sheep.
- Cows will not flee when you try to milk them.",
+ "Trained" = "You are proficient at animal handling, and can delicately handle even skittish animals without frightening them.
- Passive animals will not flee when you pick them up.",
+ "Experienced" = "You are an experienced animal caretaker with an encyclopedic knowledge of animals.",
+ "Master" = "You're a specialized animal caretaker. You can care for even the most exotic, fragile, or dangerous animals."
+ )
+
+/obj/item/food/egg/examine_skill = SKILL_HUSBANDRY
+/mob/living/simple_animal/chick/examine_skill = SKILL_HUSBANDRY
+/mob/living/simple_animal/cow/milking_skill = SKILL_HUSBANDRY
+/mob/living/simple_animal/hostile/goat/milking_skill = SKILL_HUSBANDRY
+/datum/mob_controller/passive/scooping_skill = SKILL_HUSBANDRY
+
/decl/skill/service/cooking
name = "Cooking"
desc = "Describes your skill at preparing meals and other consumable goods. This includes mixing alcoholic beverages."