Skip to content

Commit

Permalink
Merge pull request #4391 from out-of-phaze/feature/animal-husbandry
Browse files Browse the repository at this point in the history
Add animal husbandry to Shaded Hills
  • Loading branch information
MistakeNot4892 authored Aug 23, 2024
2 parents aa98e98 + 5134bf7 commit 157a496
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 35 deletions.
5 changes: 5 additions & 0 deletions code/datums/ai/_ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions code/datums/ai/passive.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
183 changes: 156 additions & 27 deletions code/modules/mob/living/simple_animal/friendly/farm_animals.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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("<span class='notice'>[user] milks [src] using \the [O].</span>")
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, "<span class='warning'>\The [O] is full.</span>")
if(!transfered)
to_chat(user, "<span class='warning'>The udder is dry. Wait a bit longer...</span>")
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
Expand All @@ -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")
Expand All @@ -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))
Expand All @@ -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!")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down
19 changes: 11 additions & 8 deletions code/modules/mob_holder/holder_mobs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<span class='warning'>You can't climb onto [target]!</span>")
return
to_chat(target, "<span class='notice'>\The [src] clambers onto you!</span>")
to_chat(initiator, "<span class='notice'>You climb up onto \the [target]!</span>")
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, "<span class='warning'>Your hands are full!</span>")
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, "<span class='notice'>You scoop up \the [src]!</span>")
to_chat(src, "<span class='notice'>\The [initiator] scoops you up!</span>")
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)
Expand Down
1 change: 1 addition & 0 deletions maps/shaded_hills/jobs/inn.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions maps/shaded_hills/jobs/wilderness.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions mods/content/fantasy/datum/skills.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.<br>- 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. <br>- 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."
Expand Down

0 comments on commit 157a496

Please sign in to comment.