diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index 5badae11bb3..c76c5650831 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -219,6 +219,9 @@ ///some behaviors that check current_target also set this on deep crit mobs #define BB_BASIC_MOB_EXECUTION_TARGET "BB_basic_execution_target" +///list of foods this mob likes +#define BB_BASIC_FOODS "BB_basic_foods" + ///Bileworm AI keys #define BB_BILEWORM_SPEW_BILE "BB_bileworm_spew_bile" diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 26b9399f65b..54f66da0c5c 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -140,13 +140,17 @@ multiple modular subtrees with behaviors idle_behavior.perform_idle_behavior(delta_time, src) //Do some stupid shit while we have nothing to do return - if(current_movement_target && get_dist(pawn, current_movement_target) > max_target_distance) //The distance is out of range - CancelActions() - return + if(current_movement_target) + if(!isatom(current_movement_target)) + stack_trace("[pawn]'s current movement target is not an atom, rather a [current_movement_target.type]! Did you accidentally set it to a weakref?") + CancelActions() + return - for(var/i in current_behaviors) - var/datum/ai_behavior/current_behavior = i + if(get_dist(pawn, current_movement_target) > max_target_distance) //The distance is out of range + CancelActions() + return + for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) // Convert the current behaviour action cooldown to realtime seconds from deciseconds.current_behavior // Then pick the max of this and the delta_time passed to ai_controller.process() diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm index f98d3508133..e8e9ca44586 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -4,12 +4,19 @@ /datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) . = ..() - controller.current_movement_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] //Hiding location is priority + //Hiding location is priority + var/datum/weakref/weak_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + if(!target) + return FALSE + controller.current_movement_target = target /datum/ai_behavior/basic_melee_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) . = ..() var/mob/living/basic/basic_mob = controller.pawn - var/atom/target = controller.blackboard[target_key] + //targetting datum will kill the action if not real anymore + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] if(!targetting_datum.can_attack(basic_mob, target)) @@ -38,23 +45,27 @@ /datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) . = ..() - controller.current_movement_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] //Hiding location is priority - + var/datum/weakref/weak_target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + if(!target) + return FALSE + controller.current_movement_target = target /datum/ai_behavior/basic_ranged_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) . = ..() var/mob/living/basic/basic_mob = controller.pawn - var/atom/target = controller.blackboard[target_key] + //targetting datum will kill the action if not real anymore + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] - if(!targetting_datum.can_attack(basic_mob, target)) finish_action(controller, FALSE, target_key) return - var/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something! + var/atom/hiding_target = targetting_datum.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something! - controller.blackboard[hiding_location_key] = hiding_target + controller.blackboard[hiding_location_key] = WEAKREF(hiding_target) if(hiding_target) //Shoot it! basic_mob.RangedAttack(hiding_target) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm index c683032db0b..6420cc6fb0c 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targetting.dm @@ -35,11 +35,11 @@ return var/atom/target = pick(filtered_targets) - controller.blackboard[target_key] = target + controller.blackboard[target_key] = WEAKREF(target) var/atom/potential_hiding_location = targetting_datum.find_hidden_mobs(living_mob, target) if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially. - controller.blackboard[hiding_location_key] = potential_hiding_location + controller.blackboard[hiding_location_key] = WEAKREF(potential_hiding_location) finish_action(controller, TRUE) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm new file mode 100644 index 00000000000..1a39315d23b --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm @@ -0,0 +1,10 @@ +/// similar to finding a target but looks for food types in the +/datum/ai_planning_subtree/find_food + +/datum/ai_planning_subtree/find_food/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(target && !QDELETED(target)) + return + var/list/wanted = controller.blackboard[BB_BASIC_FOODS] + controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list, BB_BASIC_MOB_CURRENT_TARGET, wanted) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm index 56893ce767e..4231f72a8be 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm @@ -3,7 +3,9 @@ /datum/ai_planning_subtree/basic_melee_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time) . = ..() - var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/atom/target = weak_target?.resolve() + if(!target || QDELETED(target)) return controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) @@ -15,7 +17,8 @@ /datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time) . = ..() - var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/atom/target = weak_target?.resolve() if(!target || QDELETED(target)) return controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm index e6edaaf05fa..e02f8d3bc17 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm @@ -2,7 +2,8 @@ /datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, delta_time) . = ..() - var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/atom/target = weak_target?.resolve() if(target && !QDELETED(target)) return controller.queue_behavior(/datum/ai_behavior/find_potential_targets, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) diff --git a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm index a9690524964..7eba730a82b 100644 --- a/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm +++ b/code/datums/ai/basic_mobs/targetting_datums/basic_targetting_datum.dm @@ -53,3 +53,13 @@ return TRUE return FALSE + +/// Subtype more forgiving for items. +/// Careful, this can go wrong and keep a mob hyperfocused on an item it can't lose aggro on +/datum/targetting_datum/basic/allow_items + +/datum/targetting_datum/basic/allow_items/can_attack(mob/living/living_mob, atom/the_target) + . = ..() + if(isitem(the_target)) + // trust fall exercise + return TRUE diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm new file mode 100644 index 00000000000..693d43d70d3 --- /dev/null +++ b/code/datums/ai/generic/find_and_set.dm @@ -0,0 +1,74 @@ +/**find and set + * Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something. + * if you want to do something more complicated than find a single atom, change the search_tactic() proc + * cool tip: search_tactic() can set lists + */ +/datum/ai_behavior/find_and_set + action_cooldown = 2 SECONDS + +/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller, set_key, locate_path, search_range) + . = ..() + var/find_this_thing = search_tactic(controller, locate_path, search_range) + if(find_this_thing) + controller.blackboard[set_key] = WEAKREF(find_this_thing) + finish_action(controller, TRUE) + else + finish_action(controller, FALSE) + +/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range) + return locate(locate_path) in oview(search_range, controller.pawn) + +/** + * Variant of find and set that fails if the living pawn doesn't hold something + */ +/datum/ai_behavior/find_and_set/pawn_must_hold_item + +/datum/ai_behavior/find_and_set/pawn_must_hold_item/search_tactic(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + if(!living_pawn.get_num_held_items()) + return //we want to fail the search if we don't have something held + return ..() + +/** + * Variant of find and set that also requires the item to be edible. checks hands too + */ +/datum/ai_behavior/find_and_set/edible + +/datum/ai_behavior/find_and_set/edible/search_tactic(datum/ai_controller/controller, locate_path, search_range) + var/mob/living/living_pawn = controller.pawn + var/list/food_candidates = list() + for(var/held_candidate as anything in living_pawn.held_items) + if(!held_candidate || !IsEdible(held_candidate)) + continue + food_candidates += held_candidate + + var/list/local_results = locate(locate_path) in oview(search_range, controller.pawn) + for(var/local_candidate in local_results) + if(!IsEdible(local_candidate)) + continue + food_candidates += local_candidate + if(food_candidates.len) + return pick(food_candidates) + +/** + * Variant of find and set that only checks in hands, search range should be excluded for this + */ +/datum/ai_behavior/find_and_set/in_hands + +/datum/ai_behavior/find_and_set/in_hands/search_tactic(datum/ai_controller/controller, locate_path) + var/mob/living/living_pawn = controller.pawn + return locate(locate_path) in living_pawn.held_items + +/** + * Variant of find and set that takes a list of things to find. + */ +/datum/ai_behavior/find_and_set/in_list + +/datum/ai_behavior/find_and_set/in_list/search_tactic(datum/ai_controller/controller, locate_paths, search_range) + var/list/found = list() + for(var/locate_path in locate_paths) + var/single_locate = ..(controller, locate_path, search_range) + if(single_locate) + found += single_locate + if(found.len) + return pick(found) diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 23d2ec36b3c..042153865c7 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -184,67 +184,6 @@ if(succeeded) controller.blackboard[hunger_timer_key] = world.time + rand(12 SECONDS, 60 SECONDS) -/**find and set - * Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something. - * if you want to do something more complicated than find a single atom, change the search_tactic() proc - * cool tip: search_tactic() can set lists - */ -/datum/ai_behavior/find_and_set - action_cooldown = 5 SECONDS - -/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller, set_key, locate_path, search_range) - . = ..() - var/find_this_thing = search_tactic(controller, locate_path, search_range) - if(find_this_thing) - controller.blackboard[set_key] = WEAKREF(find_this_thing) - finish_action(controller, TRUE) - else - finish_action(controller, FALSE) - -/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range) - return locate(locate_path) in oview(search_range, controller.pawn) - -/** - * Variant of find and set that fails if the living pawn doesn't hold something - */ -/datum/ai_behavior/find_and_set/pawn_must_hold_item - -/datum/ai_behavior/find_and_set/pawn_must_hold_item/search_tactic(datum/ai_controller/controller) - var/mob/living/living_pawn = controller.pawn - if(!living_pawn.get_num_held_items()) - return //we want to fail the search if we don't have something held - return ..() - -/** - * Variant of find and set that also requires the item to be edible. checks hands too - */ -/datum/ai_behavior/find_and_set/edible - -/datum/ai_behavior/find_and_set/edible/search_tactic(datum/ai_controller/controller, locate_path, search_range) - var/mob/living/living_pawn = controller.pawn - var/list/food_candidates = list() - for(var/held_candidate as anything in living_pawn.held_items) - if(!held_candidate || !IsEdible(held_candidate)) - continue - food_candidates += held_candidate - - var/list/local_results = locate(locate_path) in oview(search_range, controller.pawn) - for(var/local_candidate in local_results) - if(!IsEdible(local_candidate)) - continue - food_candidates += local_candidate - if(food_candidates.len) - return pick(food_candidates) - -/** - * Variant of find and set that only checks in hands, search range should be excluded for this - */ -/datum/ai_behavior/find_and_set/in_hands - -/datum/ai_behavior/find_and_set/in_hands/search_tactic(datum/ai_controller/controller, locate_path) - var/mob/living/living_pawn = controller.pawn - return locate(locate_path) in living_pawn.held_items - /** * Drops items in hands, very important for future behaviors that require the pawn to grab stuff */ diff --git a/code/datums/ai/making_your_ai.md b/code/datums/ai/making_your_ai.md index e5242f10914..e34153f65a4 100644 --- a/code/datums/ai/making_your_ai.md +++ b/code/datums/ai/making_your_ai.md @@ -20,7 +20,7 @@ We're simply starting out with our definition of what we're modifying. Any atom Next, we'll want to define the AI Controller. This is the "brain" of the AI. It starts as a type, but is turned into an instance once the object is instanced. -### Object Declaraction +### Object Declaration For clarity, i've included all the variables we're going to set up but haven't yet as nulls. In reality, some of these are always expected to be something and you should take a look at the base controller for which. diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm new file mode 100644 index 00000000000..5760f538751 --- /dev/null +++ b/code/datums/elements/basic_eating.dm @@ -0,0 +1,51 @@ +/** + * ## basic eating element! + * + * Small behavior for non-carbons to eat certain stuff they interact with + */ +/datum/element/basic_eating + element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH + id_arg_index = 2 + ///Path of the reagent added + var/heal_amt + /// Types the animal can eat. + var/list/food_types + +/datum/element/basic_eating/Attach(datum/target, heal_amt = 0, food_types = list()) + . = ..() + + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + + src.heal_amt = heal_amt + src.food_types = food_types + + //this lets players eat + RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, .proc/on_unarm_attack) + //this lets ai eat. yes, i'm serious + RegisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, .proc/on_pre_attackingtarget) + +/datum/element/basic_eating/Detach(datum/target) + UnregisterSignal(target, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)) + return ..() + +/datum/element/basic_eating/proc/on_unarm_attack(mob/living/eater, atom/target, proximity, modifiers) + SIGNAL_HANDLER + try_eating(eater, target) + +/datum/element/basic_eating/proc/on_pre_attackingtarget(mob/living/eater, atom/target) + SIGNAL_HANDLER + try_eating(eater, target) + +/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target) + if(eater.combat_mode) + return + if(!is_type_in_list(target, food_types)) + return + var/eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp") + var/healed = heal_amt && eater.health < eater.maxHealth + if(heal_amt) + eater.heal_overall_damage(heal_amt) + eater.visible_message(span_notice("[eater] [eat_verb]s [target]."), span_notice("You [eat_verb] [target][healed ? ", restoring some health" : ""].")) + playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE) + qdel(target) diff --git a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm new file mode 100644 index 00000000000..168a2f7544a --- /dev/null +++ b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm @@ -0,0 +1,91 @@ +//cow +/mob/living/basic/cow + name = "cow" + desc = "Known for their milk, just don't tip them over." + icon = 'icons/mob/cows.dmi' + icon_state = "cow" + icon_living = "cow" + icon_dead = "cow_dead" + icon_gib = "cow_gib" + gender = FEMALE + mob_biotypes = MOB_ORGANIC | MOB_BEAST + speak_emote = list("moos","moos hauntingly") + speed = 1.1 + see_in_dark = 6 + butcher_results = list(/obj/item/food/meat/slab = 6) + response_help_continuous = "pets" + response_help_simple = "pet" + response_disarm_continuous = "gently pushes aside" + response_disarm_simple = "gently push aside" + response_harm_continuous = "kicks" + response_harm_simple = "kick" + attack_verb_continuous = "kicks" + attack_verb_simple = "kick" + attack_sound = 'sound/weapons/punch1.ogg' + attack_vis_effect = ATTACK_EFFECT_KICK + health = 50 + maxHealth = 50 + gold_core_spawnable = FRIENDLY_SPAWN + blood_volume = BLOOD_VOLUME_NORMAL + ai_controller = /datum/ai_controller/basic_controller/cow + /// what this cow munches on, and what can be used to tame it. + var/list/food_types = list(/obj/item/food/grown/wheat) + /// message sent when tamed + var/tame_message = "lets out a happy moo" + /// singular version for player cows + var/self_tame_message = "let out a happy moo" + +/mob/living/basic/cow/Initialize(mapload) + AddComponent(/datum/component/tippable, \ + tip_time = 0.5 SECONDS, \ + untip_time = 0.5 SECONDS, \ + self_right_time = rand(25 SECONDS, 50 SECONDS), \ + post_tipped_callback = CALLBACK(src, .proc/after_cow_tipped)) + AddElement(/datum/element/pet_bonus, "moos happily!") + AddElement(/datum/element/swabable, CELL_LINE_TABLE_COW, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) + udder_component() + setup_eating() + . = ..() + ai_controller.blackboard[BB_BASIC_FOODS] = food_types + +///wrapper for the udder component addition so you can have uniquely uddered cow subtypes +/mob/living/basic/cow/proc/udder_component() + AddComponent(/datum/component/udder) + +/* + * food related components and elements are set up here for a few reasons: + * + * * static list can be created per-subtype, since static lists cannot be inherited and then changed + * * all eating-related components and elements share the same pool of food the mob likes + */ +/mob/living/basic/cow/proc/setup_eating() + var/static/list/food_types + if(!food_types) + food_types = src.food_types.Copy() + AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, .proc/tamed)) + AddElement(/datum/element/basic_eating, 10, food_types) + +/mob/living/basic/cow/proc/tamed(mob/living/tamer) + buckle_lying = 0 + visible_message("[src] [tame_message] as it seems to bond with [tamer].", "You [self_tame_message], recognizing [tamer] as your new pal.") + AddElement(/datum/element/ridable, /datum/component/riding/creature/cow) + +/* + * Proc called via callback after the cow is tipped by the tippable component. + * Begins a timer for us pleading for help. + * + * tipper - the mob who tipped us + */ +/mob/living/basic/cow/proc/after_cow_tipped(mob/living/carbon/tipper) + addtimer(CALLBACK(src, .proc/set_tip_react_blackboard, tipper), rand(10 SECONDS, 20 SECONDS)) + +/* + * We've been waiting long enough, we're going to tell our AI to begin pleading. + * + * tipper - the mob who originally tipped us + */ +/mob/living/basic/cow/proc/set_tip_react_blackboard(mob/living/carbon/tipper) + if(!HAS_TRAIT_FROM(src, TRAIT_IMMOBILIZED, TIPPED_OVER) || !ai_controller) + return + ai_controller.blackboard[BB_BASIC_MOB_TIP_REACTING] = TRUE + ai_controller.blackboard[BB_BASIC_MOB_TIPPER] = tipper diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm new file mode 100644 index 00000000000..81e1d722b01 --- /dev/null +++ b/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm @@ -0,0 +1,17 @@ +/datum/ai_controller/basic_controller/cow + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items(), + BB_BASIC_MOB_TIP_REACTING = FALSE, + BB_BASIC_MOB_TIPPER = null, + ) + + ai_traits = STOP_MOVING_WHEN_PULLED + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/tip_reaction, + /datum/ai_planning_subtree/find_food, + //attacking the food will eat it + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/random_speech/cow, + ) diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm new file mode 100644 index 00000000000..5897597c2f6 --- /dev/null +++ b/code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.dm @@ -0,0 +1,80 @@ +/// moonicorn subtype, very hostile unless there's some food to be eatin' +/mob/living/basic/cow/moonicorn + name = "moonicorn" + desc = "Magical cow of legend. Its horn will pacify anything it touches, rendering victims mostly helpless. \ + Victims, yes, because despite the enimatic and wonderous appearance, the moonicorn is incredibly aggressive." + icon_state = "moonicorn" + icon_living = "moonicorn" + icon_dead = "moonicorn_dead" + icon_gib = null //otherwise does the regular cow gib animation + faction = list("hostile") + speed = 1 + melee_damage_lower = 25 + melee_damage_upper = 25 + obj_damage = 35 + attack_verb_continuous = "telekinetically rams its moonihorn into" + attack_verb_simple = "telekinetically ram your moonihorn into" + gold_core_spawnable = NO_SPAWN + attack_sound = 'sound/weapons/bladeslice.ogg' + attack_vis_effect = ATTACK_EFFECT_SLASH + ai_controller = /datum/ai_controller/basic_controller/cow/moonicorn + food_types = list(/obj/item/food/grown/galaxythistle) + tame_message = "nods with respect" + self_tame_message = "nod with respect" + +/mob/living/basic/cow/moonicorn/Initialize(mapload) + . = ..() + AddElement(/datum/element/venomous, /datum/reagent/pax, 5) + AddElement(/datum/element/movement_turf_changer, /turf/open/floor/grass/fairy) + +/mob/living/basic/cow/moonicorn/udder_component() + AddComponent(/datum/component/udder, /obj/item/udder, null, null, /datum/reagent/drug/mushroomhallucinogen) + +/mob/living/basic/cow/moonicorn/setup_eating() + var/static/list/food_types + if(!food_types) + food_types = src.food_types.Copy() + AddElement(/datum/element/basic_eating, 10, food_types) + AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, .proc/tamed)) + +/mob/living/basic/cow/moonicorn/tamed(mob/living/tamer) + . = ..() + ///stop killing my FRIENDS + faction |= tamer.faction + +/datum/ai_controller/basic_controller/cow/moonicorn + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items/moonicorn(), + BB_BASIC_MOB_TIP_REACTING = FALSE, + BB_BASIC_MOB_TIPPER = null, + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/tip_reaction, + /datum/ai_planning_subtree/random_speech/cow, + //finds someone to kill + /datum/ai_planning_subtree/simple_find_target, + //...or something to eat, possibly. both types of target handled by melee attack subtree + /datum/ai_planning_subtree/find_food, + /datum/ai_planning_subtree/basic_melee_attack_subtree/moonicorn, + ) + +/datum/ai_planning_subtree/basic_melee_attack_subtree/moonicorn + melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/moonicorn + +/datum/ai_behavior/basic_melee_attack/moonicorn + //it's a fairly strong attack and it applies pax, so they do not attack often + action_cooldown = 2 SECONDS + +///moonicorns will not attack people holding something that could tame them. +/datum/targetting_datum/basic/allow_items/moonicorn + +/datum/targetting_datum/basic/allow_items/moonicorn/can_attack(mob/living/living_mob, atom/the_target) + . = ..() + if(!.) + return FALSE + + if(isliving(the_target)) //Targetting vs living mobs + var/mob/living/living_target = the_target + for(var/obj/item/food/grown/galaxythistle/tame_food in living_target.held_items) + return FALSE //heyyy this can tame me! let's NOT fight diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_wisdom.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_wisdom.dm new file mode 100644 index 00000000000..3436fca6006 --- /dev/null +++ b/code/modules/mob/living/basic/farm_animals/cow/cow_wisdom.dm @@ -0,0 +1,31 @@ +///Wisdom cow, gives XP to a random skill and speaks wisdoms +/mob/living/basic/cow/wisdom + name = "wisdom cow" + desc = "Known for its wisdom, shares it with all." + gold_core_spawnable = FALSE + ai_controller = /datum/ai_controller/basic_controller/cow/wisdom + +/mob/living/basic/cow/wisdom/setup_eating() + return //cannot tame me! and I don't care about eatin' nothing, neither! + +/datum/ai_controller/basic_controller/cow/wisdom + //don't give a targetting datum + blackboard = list( + BB_BASIC_MOB_TIP_REACTING = FALSE, + BB_BASIC_MOB_TIPPER = null, + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/tip_reaction, + /datum/ai_planning_subtree/random_speech/cow/wisdom, + ) + +///Give intense wisdom to the attacker if they're being friendly about it +/mob/living/basic/cow/wisdom/attack_hand(mob/living/carbon/user, list/modifiers) + if(!stat && !user.combat_mode) + to_chat(user, span_nicegreen("[src] whispers you some intense wisdoms and then disappears!")) + user.mind?.adjust_experience(pick(GLOB.skill_types), 500) + do_smoke(1, holder = src, location = get_turf(src)) + qdel(src) + return + return ..() diff --git a/code/modules/mob/living/basic/farm_animals/cows.dm b/code/modules/mob/living/basic/farm_animals/cows.dm deleted file mode 100644 index 52d513e0435..00000000000 --- a/code/modules/mob/living/basic/farm_animals/cows.dm +++ /dev/null @@ -1,189 +0,0 @@ -//cow -/mob/living/basic/cow - name = "cow" - desc = "Known for their milk, just don't tip them over." - icon = 'icons/mob/cows.dmi' - icon_state = "cow" - icon_living = "cow" - icon_dead = "cow_dead" - icon_gib = "cow_gib" - gender = FEMALE - mob_biotypes = MOB_ORGANIC | MOB_BEAST - speak_emote = list("moos","moos hauntingly") - speed = 1.1 - see_in_dark = 6 - butcher_results = list(/obj/item/food/meat/slab = 6) - response_help_continuous = "pets" - response_help_simple = "pet" - response_disarm_continuous = "gently pushes aside" - response_disarm_simple = "gently push aside" - response_harm_continuous = "kicks" - response_harm_simple = "kick" - attack_verb_continuous = "kicks" - attack_verb_simple = "kick" - attack_sound = 'sound/weapons/punch1.ogg' - attack_vis_effect = ATTACK_EFFECT_KICK - health = 50 - maxHealth = 50 - gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL - ai_controller = /datum/ai_controller/basic_controller/cow - var/tame_message = "lets out a happy moo" - var/self_tame_message = "let out a happy moo" - -/mob/living/basic/cow/Initialize(mapload) - AddComponent(/datum/component/tippable, \ - tip_time = 0.5 SECONDS, \ - untip_time = 0.5 SECONDS, \ - self_right_time = rand(25 SECONDS, 50 SECONDS), \ - post_tipped_callback = CALLBACK(src, .proc/after_cow_tipped)) - AddElement(/datum/element/pet_bonus, "moos happily!") - AddElement(/datum/element/swabable, CELL_LINE_TABLE_COW, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) - udder_component() - make_tameable() - . = ..() - -///wrapper for the udder component addition so you can have uniquely uddered cow subtypes -/mob/living/basic/cow/proc/udder_component() - AddComponent(/datum/component/udder) - -///wrapper for the tameable component addition so you can have non tamable cow subtypes -/mob/living/basic/cow/proc/make_tameable() - AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/wheat), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, .proc/tamed)) - -/mob/living/basic/cow/proc/tamed(mob/living/tamer) - buckle_lying = 0 - visible_message("[src] [tame_message] as it seems to bond with [tamer].", "You [self_tame_message], recognizing [tamer] as your new pal.") - AddElement(/datum/element/ridable, /datum/component/riding/creature/cow) - -/* - * Proc called via callback after the cow is tipped by the tippable component. - * Begins a timer for us pleading for help. - * - * tipper - the mob who tipped us - */ -/mob/living/basic/cow/proc/after_cow_tipped(mob/living/carbon/tipper) - addtimer(CALLBACK(src, .proc/set_tip_react_blackboard, tipper), rand(10 SECONDS, 20 SECONDS)) - -/* - * We've been waiting long enough, we're going to tell our AI to begin pleading. - * - * tipper - the mob who originally tipped us - */ -/mob/living/basic/cow/proc/set_tip_react_blackboard(mob/living/carbon/tipper) - if(!HAS_TRAIT_FROM(src, TRAIT_IMMOBILIZED, TIPPED_OVER) || !ai_controller) - return - ai_controller.blackboard[BB_BASIC_MOB_TIP_REACTING] = TRUE - ai_controller.blackboard[BB_BASIC_MOB_TIPPER] = tipper - -/datum/ai_controller/basic_controller/cow - blackboard = list( - BB_BASIC_MOB_TIP_REACTING = FALSE, - BB_BASIC_MOB_TIPPER = null, - ) - - ai_traits = STOP_MOVING_WHEN_PULLED - ai_movement = /datum/ai_movement/basic_avoidance - idle_behavior = /datum/idle_behavior/idle_random_walk - planning_subtrees = list( - /datum/ai_planning_subtree/tip_reaction, - /datum/ai_planning_subtree/random_speech/cow, - ) - -///Wisdom cow, gives XP to a random skill and speaks wisdoms -/mob/living/basic/cow/wisdom - name = "wisdom cow" - desc = "Known for its wisdom, shares it with all." - gold_core_spawnable = FALSE - ai_controller = /datum/ai_controller/basic_controller/cow/wisdom - -/mob/living/basic/cow/wisdom/make_tameable() - return //cannot tame me! - -/datum/ai_controller/basic_controller/cow/wisdom - planning_subtrees = list( - /datum/ai_planning_subtree/tip_reaction, - /datum/ai_planning_subtree/random_speech/cow/wisdom, - ) - -///Give intense wisdom to the attacker if they're being friendly about it -/mob/living/basic/cow/wisdom/attack_hand(mob/living/carbon/user, list/modifiers) - if(!stat && !user.combat_mode) - to_chat(user, span_nicegreen("[src] whispers you some intense wisdoms and then disappears!")) - user.mind?.adjust_experience(pick(GLOB.skill_types), 500) - do_smoke(1, holder = src, location = get_turf(src)) - qdel(src) - return - return ..() - -/mob/living/basic/cow/moonicorn - name = "moonicorn" - desc = "Magical cow of legend. Its horn will pacify anything it touches, rendering victims mostly helpless. \ - Victims, yes, because despite the enimatic and wonderous appearance, the moonicorn is incredibly aggressive." - icon_state = "moonicorn" - icon_living = "moonicorn" - icon_dead = "moonicorn_dead" - icon_gib = null //otherwise does the regular cow gib animation - faction = list("hostile") - speed = 1 - melee_damage_lower = 25 - melee_damage_upper = 25 - obj_damage = 35 - attack_verb_continuous = "telekinetically rams its moonihorn into" - attack_verb_simple = "telekinetically ram your moonihorn into" - gold_core_spawnable = NO_SPAWN - attack_sound = 'sound/weapons/bladeslice.ogg' - attack_vis_effect = ATTACK_EFFECT_SLASH - ai_controller = /datum/ai_controller/basic_controller/cow/moonicorn - tame_message = "nods with respect" - self_tame_message = "nod with respect" - -/mob/living/basic/cow/moonicorn/Initialize(mapload) - . = ..() - AddElement(/datum/element/venomous, /datum/reagent/pax, 5) - AddElement(/datum/element/movement_turf_changer, /turf/open/floor/grass/fairy) - -/mob/living/basic/cow/moonicorn/udder_component() - AddComponent(/datum/component/udder, /obj/item/udder, null, null, /datum/reagent/drug/mushroomhallucinogen) - -/mob/living/basic/cow/moonicorn/make_tameable() - AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/galaxythistle), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, .proc/tamed)) - -/mob/living/basic/cow/moonicorn/tamed(mob/living/tamer) - . = ..() - ///stop killing my FRIENDS - faction |= tamer.faction - -/datum/ai_controller/basic_controller/cow/moonicorn - blackboard = list( - BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/moonicorn(), - BB_BASIC_MOB_TIP_REACTING = FALSE, - BB_BASIC_MOB_TIPPER = null, - ) - - planning_subtrees = list( - /datum/ai_planning_subtree/tip_reaction, - /datum/ai_planning_subtree/random_speech/cow, - /datum/ai_planning_subtree/simple_find_target, - /datum/ai_planning_subtree/basic_melee_attack_subtree/moonicorn, - ) - -/datum/ai_planning_subtree/basic_melee_attack_subtree/moonicorn - melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/moonicorn - -/datum/ai_behavior/basic_melee_attack/moonicorn - //it's a fairly strong attack and it applies pax, so they do not attack often - action_cooldown = 2 SECONDS - -///moonicorns will not attack people holding something that could tame them. -/datum/targetting_datum/basic/moonicorn - -/datum/targetting_datum/basic/moonicorn/can_attack(mob/living/living_mob, atom/the_target) - . = ..() - if(!.) - return FALSE - - if(isliving(the_target)) //Targetting vs living mobs - var/mob/living/living_target = the_target - for(var/obj/item/food/grown/galaxythistle/tame_food in living_target.held_items) - return FALSE //heyyy this can tame me! let's NOT fight diff --git a/tgstation.dme b/tgstation.dme index b4579ce092f..6774a8fc52e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -678,6 +678,7 @@ #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targetting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\try_mob_ability.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\speech_subtree.dm" @@ -689,6 +690,7 @@ #include "code\datums\ai\dog\dog_behaviors.dm" #include "code\datums\ai\dog\dog_controller.dm" #include "code\datums\ai\dog\dog_subtrees.dm" +#include "code\datums\ai\generic\find_and_set.dm" #include "code\datums\ai\generic\generic_behaviors.dm" #include "code\datums\ai\generic\generic_subtrees.dm" #include "code\datums\ai\hauntium\haunted_controller.dm" @@ -976,6 +978,7 @@ #include "code\datums\elements\backblast.dm" #include "code\datums\elements\bane.dm" #include "code\datums\elements\basic_body_temp_sensitive.dm" +#include "code\datums\elements\basic_eating.dm" #include "code\datums\elements\beauty.dm" #include "code\datums\elements\bed_tucking.dm" #include "code\datums\elements\bsa_blocker.dm" @@ -3416,9 +3419,12 @@ #include "code\modules\mob\living\basic\basic.dm" #include "code\modules\mob\living\basic\basic_defense.dm" #include "code\modules\mob\living\basic\health_adjustment.dm" -#include "code\modules\mob\living\basic\farm_animals\cows.dm" #include "code\modules\mob\living\basic\farm_animals\pig.dm" #include "code\modules\mob\living\basic\farm_animals\sheep.dm" +#include "code\modules\mob\living\basic\farm_animals\cow\_cow.dm" +#include "code\modules\mob\living\basic\farm_animals\cow\cow_ai.dm" +#include "code\modules\mob\living\basic\farm_animals\cow\cow_moonicorn.dm" +#include "code\modules\mob\living\basic\farm_animals\cow\cow_wisdom.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\_bileworm.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\bileworm_actions.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\bileworm_ai.dm"