diff --git a/code/modules/mob/living/simple_animal/crow/crow.dm b/code/modules/mob/living/simple_animal/crow/crow.dm index 2ee46b3e0df2..8fd974b53c61 100644 --- a/code/modules/mob/living/simple_animal/crow/crow.dm +++ b/code/modules/mob/living/simple_animal/crow/crow.dm @@ -7,6 +7,7 @@ storage = /datum/storage/backpack/crow material = /decl/material/solid/organic/cloth +// TODO: Merge with /mob/living/simple_animal/passive/bird/crow /mob/living/simple_animal/crow name = "crow" desc = "A large crow. Caw caw." @@ -15,7 +16,7 @@ mob_size = MOB_SIZE_SMALL speak_emote = list("caws") ai = /datum/mob_controller/crow - natural_weapon = /obj/item/natural_weapon/crow_claws + natural_weapon = /obj/item/natural_weapon/bird_claws universal_speak = TRUE /datum/mob_controller/crow @@ -34,7 +35,7 @@ /mob/living/simple_animal/crow/get_bodytype() return GET_DECL(/decl/bodytype/animal/crow) -/obj/item/natural_weapon/crow_claws +/obj/item/natural_weapon/bird_claws name = "claws" gender = PLURAL attack_verb = "clawed" diff --git a/code/modules/mob_holder/_holder.dm b/code/modules/mob_holder/_holder.dm index 3cc51f873e18..26872e92c37e 100644 --- a/code/modules/mob_holder/_holder.dm +++ b/code/modules/mob_holder/_holder.dm @@ -123,9 +123,18 @@ if(length(cards)) LAZYDISTINCTADD(., cards) +/obj/item/holder/handle_mouse_drop(atom/over, mob/user, params) + if(over == user && user != src && !(user in src)) + for(var/mob/M in contents) + M.show_stripping_window(user) // TODO: verify that you can even strip items from a mob in an item + . = TRUE + . = . || ..() + /obj/item/holder/attack_self(mob/user) - for(var/mob/M in contents) - M.show_stripping_window(user) + var/mob/living/bird = locate() in contents + if(istype(bird?.ai)) + bird.ai.process_holder_interaction(user) + return TRUE /obj/item/holder/use_on_mob(mob/living/target, mob/living/user, animate = TRUE) diff --git a/mods/content/birds/_birds.dm b/mods/content/birds/_birds.dm new file mode 100644 index 000000000000..f8a627b127f1 --- /dev/null +++ b/mods/content/birds/_birds.dm @@ -0,0 +1,2 @@ +/decl/modpack/birds + name = "Birds" diff --git a/mods/content/birds/_birds.dme b/mods/content/birds/_birds.dme new file mode 100644 index 000000000000..dac5a08b08a6 --- /dev/null +++ b/mods/content/birds/_birds.dme @@ -0,0 +1,11 @@ +#ifndef MODPACK_BIRDS +#define MODPACK_BIRDS +// BEGIN_INCLUDE +#include "_birds.dm" +#include "bird.dm" +#include "bird_crow.dm" +#include "bird_hawk.dm" +#include "bird_pigeon.dm" +#include "hutch.dm" +// END_INCLUDE +#endif diff --git a/mods/content/birds/bird.dm b/mods/content/birds/bird.dm new file mode 100644 index 000000000000..b0d37f607efd --- /dev/null +++ b/mods/content/birds/bird.dm @@ -0,0 +1,25 @@ +/mob/living/simple_animal/passive/bird + mob_size = MOB_SIZE_SMALL + pass_flags = PASS_FLAG_TABLE + abstract_type = /mob/living/simple_animal/passive/bird + natural_weapon = /obj/item/natural_weapon/bird_claws + holder_type = /obj/item/holder/bird + +/obj/item/holder/bird + w_class = MOB_SIZE_SMALL + +/obj/item/holder/bird/afterattack(atom/target, mob/user, proximity) + if(proximity) + return ..() + var/mob/living/bird = locate() in contents + . = ..() + if(!user || !bird || QDELETED(src) || bird.loc != src) + return + bird.dropInto(loc) + qdel(src) // This will happen shortly regardless, but might as well skip the 1ds delay. + if(isturf(target)) + bird.visible_message(SPAN_NOTICE("\The [user] releases \a [bird]!")) + else + bird.visible_message(SPAN_NOTICE("\The [user] indicates \the [target] and releases \a [bird]!")) + if(istype(bird.ai)) + bird.ai.process_handler_target(user, target, user.get_intent()?.intent_flags) diff --git a/mods/content/birds/bird_crow.dm b/mods/content/birds/bird_crow.dm new file mode 100644 index 000000000000..32a4836eac83 --- /dev/null +++ b/mods/content/birds/bird_crow.dm @@ -0,0 +1,10 @@ +/datum/mob_controller/passive/crow + emote_speech = list("Caw.","Caw!","Caw...") + emote_hear = list("croaks", "caws") + emote_see = list("preens its feathers", "hops around") + +/mob/living/simple_animal/passive/bird/crow + name = "crow" + icon = 'mods/content/birds/icons/crow.dmi' + ai = /datum/mob_controller/passive/crow + ability_handlers = list(/datum/ability_handler/predator) // should really be /scavenger diff --git a/mods/content/birds/bird_hawk.dm b/mods/content/birds/bird_hawk.dm new file mode 100644 index 000000000000..f391e1c63e0a --- /dev/null +++ b/mods/content/birds/bird_hawk.dm @@ -0,0 +1,82 @@ +/mob/living/simple_animal/passive/bird/hawk + name = "hawk" + icon = 'mods/content/birds/icons/hawk.dmi' + ai = /datum/mob_controller/passive/hunter/hawk + ability_handlers = list(/datum/ability_handler/predator) + +/datum/mob_controller/passive/hunter/hawk + emote_speech = list("Skree!","SKREE!","Skree!?") + emote_hear = list("screeches", "screams") + emote_see = list("preens its feathers", "flicks its wings", "looks sharply around") + var/handler_set_target = FALSE + var/handling_skill = SKILL_BOTANY + var/handling_difficulty = SKILL_ADEPT + +/datum/mob_controller/passive/hunter/hawk/consume_prey(mob/living/prey) + if(prey.stat == DEAD && last_handler && handler_set_target) + set_target(last_handler?.resolve()) + prey.try_make_grab(body, defer_hand = TRUE) + return + return ..() + +/datum/mob_controller/passive/hunter/hawk/set_target(atom/new_target) + . = ..() + handler_set_target = FALSE + +/datum/mob_controller/passive/hunter/hawk/process_handler_target(mob/handler, atom/target) + if((. = ..())) + set_target(target) + handler_set_target = TRUE + process_hunting(target) + +/datum/mob_controller/passive/hunter/hawk/can_hunt(mob/living/victim) + return handler_set_target || ..() + +/datum/mob_controller/passive/hunter/hawk/check_handler_can_order(mob/handler, atom/target, intent_flags) + if(!(. = ..()) && handler.skill_check(handling_skill, handling_difficulty)) + add_friend(handler) + return ..() + +/datum/mob_controller/passive/hunter/hawk/process_handler_failure(mob/handler, atom/target) + body?.visible_message(SPAN_DANGER("\The [body] ignores \the [target] in favour of attacking \the [handler]!")) + set_target(handler) + handler_set_target = TRUE + next_hunt = 0 + return ..() + +/datum/mob_controller/passive/hunter/hawk/handle_friend_hunting(mob/user) + ..() + set_target(null) + resume_wandering() + if(!body) + return + if(body.scoop_check(user) && body.get_scooped(user, body, silent = TRUE)) + body.visible_message(SPAN_NOTICE("\The [body] alights on \the [user].")) + else + body.visible_message(SPAN_NOTICE("\The [body] lands beside \the [user].")) + + for(var/obj/item/thing in body.get_equipped_items(include_carried = TRUE)) + body.drop_from_inventory(thing) + if(!QDELETED(thing)) + user.put_in_hands(thing) + var/equipped_to = user.get_equipped_slot_for_item(thing) + var/datum/inventory_slot/slot = equipped_to && user.get_inventory_slot_datum(equipped_to) + if(istype(slot)) + to_chat(user, SPAN_NOTICE("\The [body] drops \a [thing] into your [lowertext(slot.slot_name)].")) + else + to_chat(user, SPAN_NOTICE("\The [body] drops \a [thing].")) + + return TRUE + +/datum/mob_controller/passive/hunter/hawk/process_hunting(atom/target) + // Handles pathing to the target, and attacking the target if it's a mob. + if(!(. = ..())) + return + // Maybe consider handling structures at some point? + if(isitem(target) && body.Adjacent(target)) + body.put_in_hands(target) + if(target.loc != body) + body.visible_message(SPAN_WARNING("\The [body] fails to collect \the [target]!")) + // Return to handler. + set_target(last_handler?.resolve()) + return FALSE diff --git a/mods/content/birds/bird_pigeon.dm b/mods/content/birds/bird_pigeon.dm new file mode 100644 index 000000000000..40af24aeb746 --- /dev/null +++ b/mods/content/birds/bird_pigeon.dm @@ -0,0 +1,67 @@ +/mob/living/simple_animal/passive/bird/pigeon + name = "messenger pigeon" + icon = 'mods/content/birds/icons/pigeon.dmi' + ai = /datum/mob_controller/passive/pigeon + holder_type = /obj/item/holder/bird/pigeon + var/weakref/home_hutch + +/mob/living/simple_animal/passive/bird/pigeon/Initialize() + . = ..() + update_hutch() + +/mob/living/simple_animal/passive/bird/pigeon/proc/go_home(mob/releaser) + if(!is_outside()) + return + var/obj/structure/hutch/hutch = home_hutch?.resolve() + if(!istype(hutch) || QDELETED(hutch)) + return // todo: check if the hutch is accessible from the sky + if(releaser) + releaser.visible_message(SPAN_NOTICE("\The [releaser] releases \a [src], which flutters away into the sky.")) + else + visible_message(SPAN_NOTICE("\The [src] flutters away into the sky.")) + set_dir(SOUTH) + // this is done manually due to the actual flying state primarily being handled as a movement state. + icon_state = "world-flying" + new /obj/effect/dummy/fadeout(loc, NORTH, src) + new /obj/effect/dummy/fadein(get_turf(hutch), SOUTH, src) + update_icon() + + hutch.visible_message(SPAN_NOTICE("\A [src] alights on \the [hutch] in a flutter of wings.")) + var/obj/item/holder/bird_item = new holder_type + forceMove(bird_item) + bird_item.sync(src) + hutch.storage?.handle_item_insertion(null, bird_item) + if(bird_item.loc != hutch) + dropInto(hutch.loc) + qdel(bird_item) + +/obj/item/holder/bird/pigeon/attack_self(mob/user) + var/mob/living/simple_animal/passive/bird/pigeon/pigeon = locate() in contents + if(!istype(pigeon)) + return ..() + if(!is_outside()) + to_chat(user, SPAN_WARNING("You need to be outdoors to release \the [pigeon].")) + return TRUE + if(isnull(pigeon.home_hutch)) + var/decl/pronouns/pronouns = pigeon.get_pronouns() + to_chat(user, SPAN_WARNING("\The [pigeon] tilts [pronouns.his] head at you in confusion. [pronouns.He] must not have a hutch to return to.")) + else + user.drop_from_inventory(src) + pigeon.go_home(user) + qdel(src) + return TRUE + +/mob/living/simple_animal/passive/bird/pigeon/proc/update_hutch() + var/obj/structure/hutch/hutch = home_hutch?.resolve() + if(!istype(hutch) || QDELETED(hutch)) + hutch = get_recursive_loc_of_type(/obj/structure/hutch) + if(istype(hutch) && !QDELETED(hutch)) + home_hutch = weakref(hutch) + events_repository.unregister(/decl/observ/moved, src, src) + else + events_repository.register(/decl/observ/moved, src, src, TYPE_PROC_REF(/mob/living/simple_animal/passive/bird/pigeon, update_hutch)) + +/datum/mob_controller/passive/pigeon + emote_speech = list("Oo-ooo.","Oo-ooo?","Oo-ooo...") + emote_hear = list("coos") + emote_see = list("preens its feathers", "puffs out its neck", "ruffles its wings") diff --git a/mods/content/birds/hutch.dm b/mods/content/birds/hutch.dm new file mode 100644 index 000000000000..fae7915f735f --- /dev/null +++ b/mods/content/birds/hutch.dm @@ -0,0 +1,71 @@ +/datum/storage/hutch + can_hold = list(/obj/item/holder) + max_w_class = MOB_SIZE_SMALL + storage_slots = 10 + +/datum/storage/hutch/open(mob/user) + . = ..() + var/atom/hutch = holder + if(istype(hutch)) + hutch.queue_icon_update() + +/datum/storage/hutch/close(mob/user) + . = ..() + var/atom/hutch = holder + if(istype(hutch)) + hutch.queue_icon_update() + +/obj/structure/hutch + name = "hutch" + icon = 'mods/content/birds/icons/hutch.dmi' + desc = "A hutch for containing small animals like rabbits." + icon_state = ICON_STATE_WORLD + material = /decl/material/solid/organic/wood/oak + material_alteration = MAT_FLAG_ALTERATION_ALL + storage = /datum/storage/hutch + var/initial_animal_count = 5 + var/decl/material/door_material = /decl/material/solid/organic/plantmatter/grass/dry + var/initial_animal_type + +/obj/structure/hutch/Initialize(ml, _mat, _reinf_mat) + . = ..() + + if(ispath(door_material)) + door_material = GET_DECL(door_material) + + if(initial_animal_type && initial_animal_count) + for(var/i = 1 to initial_animal_count) + var/mob/bird_type = islist(initial_animal_type) ? pick(initial_animal_type) : initial_animal_type + var/bird_holder_type = bird_type::holder_type + var/obj/item/holder/bird/bird_item = new bird_holder_type(src) + bird_item.sync(new bird_type(bird_item)) + else + update_icon() + +/obj/structure/hutch/on_update_icon() + . = ..() + if(door_material) + add_overlay(overlay_image(icon, "[icon_state]-doors-[storage?.opened ? "open" : "closed"]", door_material.color, RESET_COLOR)) + +// Bird subtypes. +/obj/structure/hutch/aviary + name = "aviary" + desc = "A hutch for containing birds like hawks or crows." + icon = 'mods/content/birds/icons/aviary.dmi' + +/obj/structure/hutch/aviary/crow + initial_animal_type = /mob/living/simple_animal/passive/bird/crow + +/obj/structure/hutch/aviary/pigeon + initial_animal_type = /mob/living/simple_animal/passive/bird/pigeon + +/obj/structure/hutch/aviary/hawk + initial_animal_type = /mob/living/simple_animal/passive/bird/hawk + +// Rabbits are a kind of bird, right? +/obj/structure/hutch/rabbit + initial_animal_type = list( + /mob/living/simple_animal/passive/rabbit, + /mob/living/simple_animal/passive/rabbit/black, + /mob/living/simple_animal/passive/rabbit/brown + ) diff --git a/mods/content/birds/icons/aviary.dmi b/mods/content/birds/icons/aviary.dmi new file mode 100644 index 000000000000..8ba4c4443f9a Binary files /dev/null and b/mods/content/birds/icons/aviary.dmi differ diff --git a/mods/content/birds/icons/crow.dmi b/mods/content/birds/icons/crow.dmi new file mode 100644 index 000000000000..6155b08b3773 Binary files /dev/null and b/mods/content/birds/icons/crow.dmi differ diff --git a/mods/content/birds/icons/hawk.dmi b/mods/content/birds/icons/hawk.dmi new file mode 100644 index 000000000000..304546724903 Binary files /dev/null and b/mods/content/birds/icons/hawk.dmi differ diff --git a/mods/content/birds/icons/hutch.dmi b/mods/content/birds/icons/hutch.dmi new file mode 100644 index 000000000000..8d5f7e2165bd Binary files /dev/null and b/mods/content/birds/icons/hutch.dmi differ diff --git a/mods/content/birds/icons/pigeon.dmi b/mods/content/birds/icons/pigeon.dmi new file mode 100644 index 000000000000..678fd450c83e Binary files /dev/null and b/mods/content/birds/icons/pigeon.dmi differ diff --git a/mods/content/fantasy/datum/skills.dm b/mods/content/fantasy/datum/skills.dm index 30736399780b..54a75cacac59 100644 --- a/mods/content/fantasy/datum/skills.dm +++ b/mods/content/fantasy/datum/skills.dm @@ -230,8 +230,11 @@ "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 +/obj/item/food/egg + examine_skill = SKILL_HUSBANDRY + +/mob/living/simple_animal/chick + examine_skill = SKILL_HUSBANDRY /datum/extension/milkable milking_skill = SKILL_HUSBANDRY diff --git a/mods/~compatibility/patches/fantasy.dm b/mods/~compatibility/patches/fantasy.dm index b0abba63bcd4..3f3ba3462e43 100644 --- a/mods/~compatibility/patches/fantasy.dm +++ b/mods/~compatibility/patches/fantasy.dm @@ -10,4 +10,9 @@ #ifdef MODPACK_BLACKSMITHY #include "fantasy/forging_fantasy.dm" -#endif \ No newline at end of file +#endif + +// Override hawk handling skill. +#ifdef MODPACK_BIRDS +#include "fantasy/bird_fantasy.dm" +#endif diff --git a/mods/~compatibility/patches/fantasy/bird_fantasy.dm b/mods/~compatibility/patches/fantasy/bird_fantasy.dm new file mode 100644 index 000000000000..f6b73ecf75a3 --- /dev/null +++ b/mods/~compatibility/patches/fantasy/bird_fantasy.dm @@ -0,0 +1,2 @@ +/datum/mob_controller/passive/hunter/hawk + handling_skill = SKILL_HUSBANDRY