diff --git a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
index c27e2caa9bf..8280c2b1f60 100644
--- a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
+++ b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm
@@ -6851,7 +6851,7 @@
/turf/simulated/floor/plating,
/area/ruin/unpowered/syndicate_space_base/engineering)
"OI" = (
-/mob/living/simple_animal/pig{
+/mob/living/basic/pig{
faction = list("neutral","syndicate")
},
/turf/simulated/floor/grass,
diff --git a/_maps/map_files/stations/boxstation.dmm b/_maps/map_files/stations/boxstation.dmm
index 4c083a45157..3e6dc065932 100644
--- a/_maps/map_files/stations/boxstation.dmm
+++ b/_maps/map_files/stations/boxstation.dmm
@@ -25807,7 +25807,7 @@
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
dir = 4
},
-/mob/living/simple_animal/pig,
+/mob/living/basic/pig,
/turf/simulated/floor/grass,
/area/station/service/hydroponics)
"bLo" = (
diff --git a/_maps/map_files/stations/cerestation.dmm b/_maps/map_files/stations/cerestation.dmm
index 996de425ca5..ed8eaf0206f 100644
--- a/_maps/map_files/stations/cerestation.dmm
+++ b/_maps/map_files/stations/cerestation.dmm
@@ -34931,7 +34931,7 @@
/turf/simulated/floor/plating,
/area/station/maintenance/fore)
"fPp" = (
-/mob/living/simple_animal/pig,
+/mob/living/basic/pig,
/turf/simulated/floor/grass,
/area/station/service/hydroponics)
"fPv" = (
diff --git a/_maps/map_files/stations/deltastation.dmm b/_maps/map_files/stations/deltastation.dmm
index 4c22039fbe1..777cef7316f 100644
--- a/_maps/map_files/stations/deltastation.dmm
+++ b/_maps/map_files/stations/deltastation.dmm
@@ -78439,7 +78439,7 @@
},
/area/station/legal/courtroom/gallery)
"qjc" = (
-/mob/living/simple_animal/pig,
+/mob/living/basic/pig,
/turf/simulated/floor/grass,
/area/station/service/kitchen)
"qjH" = (
diff --git a/_maps/map_files/stations/emeraldstation.dmm b/_maps/map_files/stations/emeraldstation.dmm
index 5262ce150fa..39ce57ef80e 100644
--- a/_maps/map_files/stations/emeraldstation.dmm
+++ b/_maps/map_files/stations/emeraldstation.dmm
@@ -120291,7 +120291,7 @@
/turf/simulated/floor/plating,
/area/station/maintenance/dorms/starboard)
"xYw" = (
-/mob/living/simple_animal/pig,
+/mob/living/basic/pig,
/turf/simulated/floor/grass,
/area/station/service/kitchen)
"xYF" = (
diff --git a/_maps/map_files/stations/metastation.dmm b/_maps/map_files/stations/metastation.dmm
index 2df5e986f53..58b6f5420f6 100644
--- a/_maps/map_files/stations/metastation.dmm
+++ b/_maps/map_files/stations/metastation.dmm
@@ -80496,7 +80496,7 @@
},
/area/station/science/xenobiology)
"uXs" = (
-/mob/living/simple_animal/pig,
+/mob/living/basic/pig,
/turf/simulated/floor/grass,
/area/station/service/hydroponics)
"uXA" = (
diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm
index 6d48c601acc..ce749d8c5a5 100644
--- a/code/__DEFINES/ai/blackboard_defines.dm
+++ b/code/__DEFINES/ai/blackboard_defines.dm
@@ -6,6 +6,17 @@
#define BB_CURRENT_MIN_MOVE_DISTANCE "BB_min_move_distance"
/// How likely is this mob to move when idle per tick?
#define BB_BASIC_MOB_IDLE_WALK_CHANCE "BB_basic_idle_walk_chance"
+/// Flag to set on if you want your mob to STOP running away
+#define BB_BASIC_MOB_STOP_FLEEING "BB_basic_stop_fleeing"
+/// Key of something we're running away from
+#define BB_BASIC_MOB_FLEE_TARGET "BB_basic_flee_target"
+/// Key for the current hidden location of something we're running away from
+#define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location"
+/// Key defining the targeting strategy for things to flee from
+#define BB_FLEE_TARGETING_STRATEGY "BB_flee_targeting_strategy"
+/// Key defining how far we attempt to get away from something we're fleeing from
+#define BB_BASIC_MOB_FLEE_DISTANCE "BB_basic_flee_distance"
+#define DEFAULT_BASIC_FLEE_DISTANCE 9
// Searching
@@ -14,12 +25,22 @@
// Targeting
+/// How close a mob must be for us to select it as a target, if that is less than how far we can maintain it as a target
+#define BB_AGGRO_RANGE "BB_aggro_range"
+/// Key for our current target.
+#define BB_BASIC_MOB_CURRENT_TARGET "BB_basic_current_target"
+/// Key for the current hidden location of our target if applicable.
+#define BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION "BB_basic_current_target_hiding_location"
+/// Key for the type of targeting strategy we will apply.
+#define BB_TARGETING_STRATEGY "targeting_strategy"
/// Key for the minimum status at which we want to target mobs (does not need to be specified if CONSCIOUS)
#define BB_TARGET_MINIMUM_STAT "BB_target_minimum_stat"
/// Should we skip the faction check for the targeting strategy?
#define BB_ALWAYS_IGNORE_FACTION "BB_always_ignore_factions"
/// Are we in some kind of temporary state of ignoring factions when targeting? can result in volatile results if multiple behaviours touch this
#define BB_TEMPORARILY_IGNORE_FACTION "BB_temporarily_ignore_factions"
+///List of mobs who have damaged us
+#define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist"
// Food and eating
@@ -40,3 +61,8 @@
#define BB_BASIC_MOB_TIP_REACTING "BB_basic_tip_reacting"
/// the motherfucker who tipped us
#define BB_BASIC_MOB_TIPPER "BB_basic_tip_tipper"
+
+// Misc
+
+/// For /datum/ai_behavior/find_potential_targets, what if any field are we using currently
+#define BB_FIND_TARGETS_FIELD(type) "bb_find_targets_field_[type]"
diff --git a/code/__DEFINES/dcs/ai_signals.dm b/code/__DEFINES/dcs/ai_signals.dm
index b6a3ce5e399..b9d52316d7c 100644
--- a/code/__DEFINES/dcs/ai_signals.dm
+++ b/code/__DEFINES/dcs/ai_signals.dm
@@ -12,6 +12,7 @@
//from base of atom/attack_basic_mob(): (/mob/user)
#define COMSIG_ATOM_ATTACK_BASIC_MOB "attack_basic_mob"
+ #define COMPONENT_BASIC_ATTACK_CANCEL_CHAIN (1<<0)
///sent from ai controllers when they possess a pawn: (datum/ai_controller/source_controller)
#define COMSIG_AI_CONTROLLER_POSSESSED_PAWN "ai_controller_possessed_pawn"
diff --git a/code/__DEFINES/dcs/atom_signals.dm b/code/__DEFINES/dcs/atom_signals.dm
index a1fc6449a75..39bca29e114 100644
--- a/code/__DEFINES/dcs/atom_signals.dm
+++ b/code/__DEFINES/dcs/atom_signals.dm
@@ -98,6 +98,15 @@
#define COMSIG_ATOM_PREHIT "atom_prehit"
#define ATOM_PREHIT_SUCCESS (1<<0)
#define ATOM_PREHIT_FAILURE (1<<1)
+///from relay_attackers element: (atom/attacker, attack_flags)
+#define COMSIG_ATOM_ATTACK_ANIMAL "atom_attack_animal"
+#define COMSIG_ATOM_WAS_ATTACKED "atom_was_attacked"
+ ///The damage type of the weapon projectile is non-lethal stamina
+ #define ATTACKER_STAMINA_ATTACK (1<<0)
+ ///the attacker is shoving the source
+ #define ATTACKER_SHOVING (1<<1)
+ /// The attack is a damaging-type attack
+ #define ATTACKER_DAMAGING_ATTACK (1<<2)
/// Called from atom/Initialize() of target: (atom/target)
#define COMSIG_ATOM_INITIALIZED_ON "atom_initialized_on"
diff --git a/code/__DEFINES/dcs/attack_chain_signals.dm b/code/__DEFINES/dcs/attack_chain_signals.dm
index 64cbef3243a..51210b9074d 100644
--- a/code/__DEFINES/dcs/attack_chain_signals.dm
+++ b/code/__DEFINES/dcs/attack_chain_signals.dm
@@ -47,6 +47,15 @@
#define COMSIG_AFTER_ATTACK "item_after_attack" //! [/obj/item/proc/afterattack] -> atom/target, mob/user, params
#define COMSIG_AFTER_ATTACKED_BY "after_attacked_by" //! [/obj/item/proc/afterattack] -> obj/item/weapon, mob/user, proximity_flag, params
+/// Before attackingtarget has happened, source is the attacker and target is the attacked
+#define COMSIG_HOSTILE_PRE_ATTACKINGTARGET "hostile_pre_attackingtarget"
+ #define COMPONENT_HOSTILE_NO_ATTACK COMPONENT_CANCEL_ATTACK_CHAIN //cancel the attack, only works before attack happens
+#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget"
+/// Called when a /mob/living/simple_animal/hostile fines a new target: (atom/source, give_target)
+#define COMSIG_HOSTILE_FOUND_TARGET "comsig_hostile_found_target"
+/// After attackingtarget has happened, source is the attacker and target is the attacked, extra argument for if the attackingtarget was successful
+#define COMSIG_HOSTILE_POST_ATTACKINGTARGET "hostile_post_attackingtarget"
+
// Return values for directing the control of the attack chain. Distinct from
// signal interceptors because they're not meant to be combined, and to mesh better with
// historical use of return values in attack chain procs.
diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm
index b978c2dcde8..a92233a371c 100644
--- a/code/__DEFINES/dcs/mob_signals.dm
+++ b/code/__DEFINES/dcs/mob_signals.dm
@@ -188,12 +188,6 @@
///from /mob/living/simple_animal/handle_environment()
#define COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT "simpleanimal_handle_environment"
-// /mob/living/simple_animal/hostile signals
-#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget"
- #define COMPONENT_HOSTILE_NO_ATTACK (1<<0)
-//Called when a /mob/living/simple_animal/hostile fines a new target: (atom/source, give_target)
-#define COMSIG_HOSTILE_FOUND_TARGET "comsig_hostile_found_target"
-
///from of mob/MouseDrop(): (/atom/over, /mob/user)
#define COMSIG_DO_MOB_STRIP "do_mob_strip"
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index 054eccfbc72..b28856ea2f3 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -130,6 +130,9 @@
#define HAS_MIND_TRAIT(target, trait) (istype(target, /datum/mind) ? HAS_TRAIT(target, trait) : (target.mind ? HAS_TRAIT(target.mind, trait) : FALSE))
/// Gives a unique trait source for any given datum
#define UNIQUE_TRAIT_SOURCE(target) "unique_source_[target.UID()]"
+/// Returns a list of trait sources for this trait. Only useful for wacko cases and internal futzing
+/// You should not be using this
+#define GET_TRAIT_SOURCES(target, trait) (target.status_traits?[trait] || list())
/*
Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits.
@@ -326,6 +329,13 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// A general trait for tracking whether a zombie owned the organ or limb
#define TRAIT_I_WANT_BRAINS_ORGAN "zombie_organ"
+
+/// Trait given by /datum/element/relay_attacker
+#define TRAIT_RELAYING_ATTACKER "relaying_attacker"
+
+/// Trait applied to a mob when it gets a required "operational datum" (components/elements). Sends out the source as the type of the element.
+#define TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM "element-required"
+
//****** OBJ TRAITS *****//
///An /obj that should not increase the "depth" of the search for adjacency,
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 397cda59005..9f41c3c43c3 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -113,7 +113,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK,
"TRAIT_RECENTLY_BLOCKED_MAGIC" = TRAIT_RECENTLY_BLOCKED_MAGIC,
"TRAIT_UNKNOWN" = TRAIT_UNKNOWN,
- "TRAIT_EXAMINE_HALLUCINATING" = TRAIT_EXAMINE_HALLUCINATING
+ "TRAIT_EXAMINE_HALLUCINATING" = TRAIT_EXAMINE_HALLUCINATING,
+ "TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM" = TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM,
+ "TRAIT_RELAYING_ATTACKER" = TRAIT_RELAYING_ATTACKER,
),
/datum/mind = list(
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 364cdee730d..552c4353513 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -66,14 +66,29 @@
if(sigreturn & COMPONENT_SKIP_ATTACK)
return FALSE
- target.attack_animal(src)
+ resolve_unarmed_attack(target, modifiers)
/mob/living/simple_animal/hostile/UnarmedAttack(atom/A)
target = A
AttackingTarget()
+/mob/living/proc/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ attack_target.attack_animal(src, modifiers)
+
/atom/proc/attack_animal(mob/user)
- return
+ SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_ANIMAL, user)
+
+///When a basic mob attacks something, either by AI or user.
+/atom/proc/attack_basic_mob(mob/user, list/modifiers)
+ SHOULD_CALL_PARENT(TRUE)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_BASIC_MOB, user) & COMPONENT_BASIC_ATTACK_CANCEL_CHAIN)
+ return
+ return handle_basic_attack(user, modifiers) //return value of attack animal, this is how much damage was dealt to the attacked thing
+
+///This exists so stuff can override the default call of attack_animal for attack_basic_mob
+///Remove this when simple animals are removed and everything can be handled on attack basic mob.
+/atom/proc/handle_basic_attack(user, modifiers)
+ return attack_animal(user, modifiers)
/mob/living/RestrainedClickOn(atom/A)
return
diff --git a/code/datums/ai/ai_planning_subtree.dm b/code/datums/ai/ai_planning_subtree.dm
index 4ab224cd8d7..10e45a49257 100644
--- a/code/datums/ai/ai_planning_subtree.dm
+++ b/code/datums/ai/ai_planning_subtree.dm
@@ -3,6 +3,15 @@
/// subtype and modify select_behaviors() without needing to subtype the AI
/// controller itself.
/datum/ai_planning_subtree
+ /// A list of typepaths of "operational datums" (elements/components) we
+ /// absolutely NEED to run. Checked in unit tests, as well as be a nice
+ /// reminder to developers that such a thing might be needed. Note that in
+ /// the Attach/Inititalize/New (or any future equivalent for these procs),
+ /// you will need to add the trait TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM
+ /// to the mob in question in order for unit tests to succeed. This will
+ /// break obviously enough if you don't do this and declare the required
+ /// datum here however.
+ var/list/operational_datums = null
/// Determines what behaviors should the controller try processing; if this
/// returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through
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
new file mode 100644
index 00000000000..e32c8135a23
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm
@@ -0,0 +1,147 @@
+/datum/ai_behavior/basic_melee_attack
+ action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ ///do we finish this action after hitting once?
+ var/terminate_after_action = FALSE
+
+/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ if(!controller.blackboard[targeting_strategy_key])
+ CRASH("No targeting strategy was supplied in the blackboard for [controller.pawn]")
+ //Hiding location is priority
+ var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ if(isliving(controller.pawn))
+ var/mob/living/pawn = controller.pawn
+ if(world.time < pawn.next_move)
+ return AI_BEHAVIOR_INSTANT
+
+ var/mob/living/basic/basic_mob = controller.pawn
+ //targeting strategy will kill the action if not real anymore
+ var/atom/target = controller.blackboard[target_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+
+ if(!targeting_strategy.can_attack(basic_mob, target))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
+
+ controller.set_blackboard_key(hiding_location_key, hiding_target)
+
+ var/atom/final_target = hiding_target || target
+ controller.ai_interact(target = final_target, intent = INTENT_HARM)
+ if(terminate_after_action)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+ return AI_BEHAVIOR_DELAY
+
+/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ if(!succeeded)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/basic_melee_attack/interact_once
+ terminate_after_action = TRUE
+
+/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/basic_ranged_attack
+ action_cooldown = 0.6 SECONDS
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
+ required_distance = 3
+ /// range we will try chasing the target before giving up
+ var/chase_range = 9
+ ///do we care about avoiding friendly fire?
+ var/avoid_friendly_fire = FALSE
+
+/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ if(HAS_TRAIT(controller.pawn, TRAIT_HANDS_BLOCKED))
+ return FALSE
+ var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ var/mob/living/basic/basic_mob = controller.pawn
+ //targeting strategy will kill the action if not real anymore
+ var/atom/target = controller.blackboard[target_key]
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+
+ if(!targeting_strategy.can_attack(basic_mob, target, chase_range))
+ return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
+
+ var/atom/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
+ var/atom/final_target = hiding_target ? hiding_target : target
+
+ if(!can_see(basic_mob, final_target, required_distance))
+ return AI_BEHAVIOR_INSTANT
+
+ if(avoid_friendly_fire && check_friendly_in_path(basic_mob, target, targeting_strategy))
+ adjust_position(basic_mob, target)
+ return AI_BEHAVIOR_DELAY
+
+ controller.set_blackboard_key(hiding_location_key, hiding_target)
+ basic_mob.RangedAttack(final_target)
+ return AI_BEHAVIOR_DELAY //only start the cooldown when the shot is shot
+
+/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ if(!succeeded)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/basic_ranged_attack/proc/check_friendly_in_path(mob/living/source, atom/target, datum/targeting_strategy/targeting_strategy)
+ var/list/turfs_list = calculate_trajectory(source, target)
+ for(var/turf/possible_turf as anything in turfs_list)
+
+ for(var/mob/living/potential_friend in possible_turf)
+ if(!targeting_strategy.can_attack(source, potential_friend))
+ return TRUE
+
+ return FALSE
+
+/datum/ai_behavior/basic_ranged_attack/proc/adjust_position(mob/living/living_pawn, atom/target)
+ var/turf/our_turf = get_turf(living_pawn)
+ var/list/possible_turfs = list()
+
+ for(var/direction in GLOB.alldirs)
+ var/turf/target_turf = get_step(our_turf, direction)
+ if(isnull(target_turf))
+ continue
+ if(is_blocked_turf(target_turf) || get_dist(target_turf, target) > get_dist(living_pawn, target))
+ continue
+ possible_turfs += target_turf
+
+ if(!length(possible_turfs))
+ return
+ var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target)
+ step(living_pawn, get_dir(living_pawn, picked_turf))
+
+/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target)
+ var/list/turf_list = get_line(source, target)
+ var/list_length = length(turf_list) - 1
+ for(var/i in 1 to list_length)
+ var/turf/current_turf = turf_list[i]
+ var/turf/next_turf = turf_list[i + 1]
+ var/direction_to_turf = get_dir(current_turf, next_turf)
+ if(!IS_DIR_DIAGONAL(direction_to_turf))
+ continue
+
+ for(var/cardinal_direction in GLOB.cardinal)
+ if(cardinal_direction & direction_to_turf)
+ turf_list += get_step(current_turf, cardinal_direction)
+
+ turf_list -= get_turf(source)
+ turf_list -= get_turf(target)
+
+ return turf_list
+
+/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire
+ avoid_friendly_fire = TRUE
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm
new file mode 100644
index 00000000000..8a570375cba
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm
@@ -0,0 +1,13 @@
+/// Picks targets based on which one is closest to you, choice between targets at equal distance is arbitrary
+/datum/ai_behavior/find_potential_targets/nearest
+
+/datum/ai_behavior/find_potential_targets/nearest/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
+ var/turf/our_position = get_turf(controller.pawn)
+ return get_closest_atom(/atom/, filtered_targets, our_position)
+
+/// As above but targets have been filtered from the 'retaliate' blackboard list
+/datum/ai_behavior/target_from_retaliate_list/nearest
+
+/datum/ai_behavior/target_from_retaliate_list/nearest/pick_final_target(datum/ai_controller/controller, list/enemies_list)
+ var/turf/our_position = get_turf(controller.pawn)
+ return get_closest_atom(/atom/, enemies_list, our_position)
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
new file mode 100644
index 00000000000..09958cd987c
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm
@@ -0,0 +1,75 @@
+/// Move to a position further away from your current target
+/datum/ai_behavior/run_away_from_target
+ required_distance = 0
+ action_cooldown = 0
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// How far do we try to run? Further makes for smoother running, but potentially weirder pathfinding
+ var/run_distance = DEFAULT_BASIC_FLEE_DISTANCE
+ /// Clear target if we finish the action unsuccessfully
+ var/clear_failed_targets = TRUE
+
+/datum/ai_behavior/run_away_from_target/setup(datum/ai_controller/controller, target_key, hiding_location_key)
+ var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || initial(run_distance)
+ if(!plot_path_away_from(controller, target))
+ return FALSE
+ return ..()
+
+/datum/ai_behavior/run_away_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
+ if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ return AI_BEHAVIOR_DELAY
+ var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
+ if(QDELETED(target) || !can_see(controller.pawn, target, run_distance))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+ if(get_dist(controller.pawn, controller.current_movement_target) > required_distance)
+ return AI_BEHAVIOR_DELAY // Still heading over
+ if(plot_path_away_from(controller, target))
+ return AI_BEHAVIOR_DELAY
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+/datum/ai_behavior/run_away_from_target/proc/plot_path_away_from(datum/ai_controller/controller, atom/target)
+ var/turf/target_destination = get_turf(controller.pawn)
+ var/static/list/offset_angles = list(45, 90, 135, 180, 225, 270)
+ for(var/angle in offset_angles)
+ var/turf/test_turf = get_furthest_turf(controller.pawn, angle, target)
+ if(isnull(test_turf))
+ continue
+ var/distance_from_target = get_dist(target, test_turf)
+ if(distance_from_target <= get_dist(target, target_destination))
+ continue
+ target_destination = test_turf
+ if(distance_from_target == run_distance) //we already got the max running distance
+ break
+
+ if(target_destination == get_turf(controller.pawn))
+ return FALSE
+ set_movement_target(controller, target_destination)
+ return TRUE
+
+/datum/ai_behavior/run_away_from_target/proc/get_furthest_turf(atom/source, angle, atom/target)
+ var/turf/return_turf
+ for(var/i in 1 to run_distance)
+ var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle)
+ if(is_blocked_turf(test_destination, excluded_objs = GLOB.airlocks + src))
+ break
+ return_turf = test_destination
+ return return_turf
+
+/datum/ai_behavior/run_away_from_target/finish_action(datum/ai_controller/controller, succeeded, target_key, hiding_location_key)
+ . = ..()
+ if(clear_failed_targets)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/run_away_from_target/run_and_shoot
+ clear_failed_targets = FALSE
+
+/datum/ai_behavior/run_away_from_target/run_and_shoot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+ var/mob/living/living_pawn = controller.pawn
+ living_pawn.RangedAttack(target)
+ return ..()
+
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
new file mode 100644
index 00000000000..637f15c1334
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm
@@ -0,0 +1,158 @@
+/// List of objects that AIs will treat as targets
+GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
+/// Static typecache list of things we are interested in
+/// Consider this a union of the for loop and the hearers call from below
+/// Must be kept up to date with the contents of hostile_machines
+GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(
+ /mob,
+ /obj/machinery/porta_turret,
+ /obj/mecha,
+)))
+
+/datum/ai_behavior/find_potential_targets
+ action_cooldown = 2 SECONDS
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// How far can we see stuff?
+ var/vision_range = 9
+ /// Blackboard key for aggro range, uses vision range if not specified
+ var/aggro_range_key = BB_AGGRO_RANGE
+
+/datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for)
+ if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)])
+ return 60 SECONDS
+ return ..()
+
+/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ var/mob/living/living_mob = controller.pawn
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+
+ if(!targeting_strategy)
+ CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
+
+ var/atom/current_target = controller.blackboard[target_key]
+ if(targeting_strategy.can_attack(living_mob, current_target, vision_range))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+
+ controller.clear_blackboard_key(target_key)
+
+ // If we're using a field rn, just don't do anything yeah?
+ if(controller.blackboard[BB_FIND_TARGETS_FIELD(type)])
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide
+
+ for(var/atom/hostile_machine as anything in GLOB.hostile_machines)
+ if(can_see(living_mob, hostile_machine, aggro_range))
+ potential_targets += hostile_machine
+
+ if(!length(potential_targets))
+ failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/list/filtered_targets = list()
+
+ for(var/atom/pot_target in potential_targets)
+ if(targeting_strategy.can_attack(living_mob, pot_target))//Can we attack it?
+ filtered_targets += pot_target
+ continue
+
+ if(!length(filtered_targets))
+ failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/atom/target = pick_final_target(controller, filtered_targets)
+ controller.set_blackboard_key(target_key, target)
+
+ var/atom/potential_hiding_location = targeting_strategy.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.set_blackboard_key(hiding_location_key, potential_hiding_location)
+
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
+ // takes the larger between our range() input and our implicit hearers() input (world.view)
+ aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2))
+ // Alright, here's the interesting bit
+ // We're gonna use this max range to hook into a proximity field so we can just await someone interesting to come along
+ // Rather then trying to check every few seconds
+ var/datum/proximity_monitor/advanced/ai_target_tracking/detection_field = new(
+ controller.pawn,
+ aggro_range,
+ TRUE,
+ src,
+ controller,
+ target_key,
+ targeting_strategy_key,
+ hiding_location_key,
+ )
+ // We're gonna store this field in our blackboard, so we can clear it away if we end up finishing successsfully
+ controller.set_blackboard_key(BB_FIND_TARGETS_FIELD(type), detection_field)
+
+/datum/ai_behavior/find_potential_targets/proc/new_turf_found(turf/found, datum/ai_controller/controller, datum/targeting_strategy/strategy)
+ var/valid_found = FALSE
+ var/mob/pawn = controller.pawn
+ for(var/maybe_target as anything in found)
+ if(maybe_target == pawn)
+ continue
+ if(!is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
+ continue
+ if(!strategy.can_attack(pawn, maybe_target))
+ continue
+ valid_found = TRUE
+ break
+ if(!valid_found)
+ return
+ // If we found any one thing we "could" attack, then run the full search again so we can select from the best possible canidate
+ var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
+ qdel(field) // autoclears so it's fine
+ // Fire instantly, you should find something I hope
+ controller.modify_cooldown(src, world.time)
+
+/datum/ai_behavior/find_potential_targets/proc/atom_allowed(atom/movable/checking, datum/targeting_strategy/strategy, mob/pawn)
+ if(checking == pawn)
+ return FALSE
+ if(!ismob(checking) && !is_type_in_typecache(checking, GLOB.target_interested_atoms))
+ return FALSE
+ if(!strategy.can_attack(pawn, checking))
+ return FALSE
+ return TRUE
+
+/datum/ai_behavior/find_potential_targets/proc/new_atoms_found(list/atom/movable/found, datum/ai_controller/controller, target_key, datum/targeting_strategy/strategy, hiding_location_key)
+ var/mob/pawn = controller.pawn
+ var/list/accepted_targets = list()
+ for(var/maybe_target as anything in found)
+ if(maybe_target == pawn)
+ continue
+ // Need to better handle viewers here
+ if(!ismob(maybe_target) && !is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
+ continue
+ if(!strategy.can_attack(pawn, maybe_target))
+ continue
+ accepted_targets += maybe_target
+
+ // Alright, we found something acceptable, let's use it yeah?
+ var/atom/target = pick_final_target(controller, accepted_targets)
+ controller.set_blackboard_key(target_key, target)
+
+ var/atom/potential_hiding_location = strategy.find_hidden_mobs(pawn, 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.set_blackboard_key(hiding_location_key, potential_hiding_location)
+
+ finish_action(controller, succeeded = TRUE)
+
+/datum/ai_behavior/find_potential_targets/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ if(succeeded)
+ var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
+ qdel(field) // autoclears so it's fine
+ controller.cancel_actions() // On retarget cancel any further queued actions so that they will setup again with new target
+ controller.modify_cooldown(src, get_cooldown(controller))
+
+/// Returns the desired final target from the filtered list of targets
+/datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
+ return pick(filtered_targets)
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
new file mode 100644
index 00000000000..e0762fccde5
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm
@@ -0,0 +1,25 @@
+/// Try to escape from your current target, without performing any other actions.
+/datum/ai_planning_subtree/flee_target
+ /// Behaviour to execute in order to flee
+ var/flee_behaviour = /datum/ai_behavior/run_away_from_target
+ /// Blackboard key in which to store selected target
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// Blackboard key in which to store selected target's hiding place
+ var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
+
+/datum/ai_planning_subtree/flee_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/atom/flee_from = controller.blackboard[target_key]
+ if(!should_flee(controller, flee_from))
+ return
+ var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
+ if(get_dist(controller.pawn, flee_from) >= flee_distance)
+ return
+
+ controller.queue_behavior(flee_behaviour, target_key, hiding_place_key)
+ return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here.
+
+/datum/ai_planning_subtree/flee_target/proc/should_flee(datum/ai_controller/controller, atom/flee_from)
+ if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING] || QDELETED(flee_from))
+ return FALSE
+ return TRUE
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm
index b2084f56002..faaf5d458db 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm
@@ -53,11 +53,3 @@
if(length(speak_verbs))
speak_verb = pick(speak_verbs)
controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play, speak_verb)
-
-/datum/ai_planning_subtree/random_speech/cow
- speech_chance = 2
- speak = list("Moo?", "Moo", "MOOOOOO")
- speak_verbs = list("moos", "moos hauntingly")
- sound = list('sound/creatures/cow.ogg')
- emote_hear = list("brays.")
- emote_see = list("shakes her head.")
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
new file mode 100644
index 00000000000..e5901af13c9
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm
@@ -0,0 +1,13 @@
+/datum/ai_planning_subtree/basic_melee_attack_subtree
+ /// What do we do in order to attack someone?
+ var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
+ /// Is this the last thing we do? (if we set a movement target, this will usually be yes)
+ var/end_planning = TRUE
+
+/datum/ai_planning_subtree/basic_melee_attack_subtree/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
+ return
+ controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+ if(end_planning)
+ return SUBTREE_RETURN_FINISH_PLANNING // we are going into battle...no distractions.
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
new file mode 100644
index 00000000000..f698ac7838e
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm
@@ -0,0 +1,25 @@
+/// Find the nearest thing which we assume is hostile and set it as the flee target
+/datum/ai_planning_subtree/simple_find_nearest_target_to_flee
+
+/datum/ai_planning_subtree/simple_find_nearest_target_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ return
+ controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+
+/// Find the nearest thing on our list of 'things which have done damage to me' and set it as the flee target
+/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee
+ ///the targeting strategy we use
+ var/targeting_key = BB_TARGETING_STRATEGY
+ ///what key should we set the target as
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+
+/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ return
+ controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list/nearest, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
+
+/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/from_flee_key
+ target_key = BB_BASIC_MOB_FLEE_TARGET
+ targeting_key = BB_FLEE_TARGETING_STRATEGY
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
new file mode 100644
index 00000000000..42a4e70081c
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm
@@ -0,0 +1,80 @@
+/// Sets the BB target to a mob which you can see and who has recently attacked you
+/datum/ai_planning_subtree/target_retaliate
+ operational_datums = list(/datum/element/ai_retaliate, /datum/component/ai_retaliate_advanced)
+ /// Blackboard key which tells us how to select valid targets
+ var/targeting_strategy_key = BB_TARGETING_STRATEGY
+ /// Blackboard key in which to store selected target
+ var/target_key = BB_BASIC_MOB_CURRENT_TARGET
+ /// Blackboard key in which to store selected target's hiding place
+ var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
+ /// do we check for faction?
+ var/check_faction = FALSE
+
+/datum/ai_planning_subtree/target_retaliate/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction)
+
+/datum/ai_planning_subtree/target_retaliate/check_faction
+ check_faction = TRUE
+
+/// Places a mob which you can see and who has recently attacked you into some 'run away from this' AI keys
+/// Can use a different targeting strategy than you use to select attack targets
+/// Not required if fleeing is the only target behaviour or uses the same target datum
+/datum/ai_planning_subtree/target_retaliate/to_flee
+ targeting_strategy_key = BB_FLEE_TARGETING_STRATEGY
+ target_key = BB_BASIC_MOB_FLEE_TARGET
+ hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION
+
+/**
+ * Picks a target from a provided list of atoms who have been pissing you off
+ * You will probably need /datum/element/ai_retaliate to take advantage of this unless you're populating the blackboard yourself
+ */
+/datum/ai_behavior/target_from_retaliate_list
+ action_cooldown = 2 SECONDS
+ /// How far can we see stuff?
+ var/vision_range = 9
+
+/datum/ai_behavior/target_from_retaliate_list/perform(seconds_per_tick, datum/ai_controller/controller, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
+ var/mob/living/living_mob = controller.pawn
+ var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+ if(!targeting_strategy)
+ . = AI_BEHAVIOR_DELAY
+ CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
+
+ var/list/shitlist = controller.blackboard[shitlist_key]
+ var/atom/existing_target = controller.blackboard[target_key]
+
+ if(!check_faction)
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
+
+ if(!QDELETED(existing_target) && (locate(existing_target) in shitlist) && targeting_strategy.can_attack(living_mob, existing_target, vision_range))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+ var/list/enemies_list = list()
+ for(var/mob/living/potential_target as anything in shitlist)
+ if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range))
+ continue
+ enemies_list += potential_target
+
+ if(!length(enemies_list))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/atom/new_target = pick_final_target(controller, enemies_list)
+ controller.set_blackboard_key(target_key, new_target)
+
+ var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, new_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.set_blackboard_key(hiding_location_key, potential_hiding_location)
+
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+/// Returns the desired final target from the filtered list of enemies
+/datum/ai_behavior/target_from_retaliate_list/proc/pick_final_target(datum/ai_controller/controller, list/enemies_list)
+ return pick(enemies_list)
+
+/datum/ai_behavior/target_from_retaliate_list/finish_action(datum/ai_controller/controller, succeeded, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
+ . = ..()
+ if(succeeded || check_faction)
+ return
+ var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)
diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm
new file mode 100644
index 00000000000..c06617b938f
--- /dev/null
+++ b/code/datums/components/ai_retaliate_advanced.dm
@@ -0,0 +1,37 @@
+/**
+ * Attached to a mob with an AI controller, passes things which have damaged it to a blackboard.
+ * The AI controller is responsible for doing anything with that information.
+ * Differs from the element as it passes new entries through a callback.
+ */
+/datum/component/ai_retaliate_advanced
+ /// Callback to a mob for custom behaviour
+ var/datum/callback/post_retaliate_callback
+
+/datum/component/ai_retaliate_advanced/Initialize(datum/callback/post_retaliate_callback)
+ if(!ismob(parent))
+ return ELEMENT_INCOMPATIBLE
+
+ src.post_retaliate_callback = post_retaliate_callback
+ parent.AddElement(/datum/element/relay_attackers)
+
+ ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
+/datum/component/ai_retaliate_advanced/Destroy(force)
+ post_retaliate_callback = null
+ return ..()
+
+/datum/component/ai_retaliate_advanced/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+
+/datum/component/ai_retaliate_advanced/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_ATOM_WAS_ATTACKED)
+
+/// Add an attacking atom to a blackboard list of things which attacked us
+/datum/component/ai_retaliate_advanced/proc/on_attacked(mob/victim, atom/attacker)
+ SIGNAL_HANDLER
+
+ if(!victim.ai_controller)
+ return
+
+ victim.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
+ post_retaliate_callback?.InvokeAsync(attacker)
diff --git a/code/datums/elements/ai_flee_while_injured.dm b/code/datums/elements/ai_flee_while_injured.dm
new file mode 100644
index 00000000000..75fc47f7f14
--- /dev/null
+++ b/code/datums/elements/ai_flee_while_injured.dm
@@ -0,0 +1,46 @@
+/**
+ * Attached to a mob with an AI controller, simply sets a flag on whether or not to run away based on current health values.
+ */
+/datum/element/ai_flee_while_injured
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// Health value to end fleeing if at or above
+ var/stop_fleeing_at
+ /// Health value to start fleeing if at or below
+ var/start_fleeing_below
+
+/datum/element/ai_flee_while_injured/Attach(datum/target, stop_fleeing_at = 1, start_fleeing_below = 0.5)
+ . = ..()
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ var/mob/living/living_target = target
+ if(!living_target.ai_controller)
+ return ELEMENT_INCOMPATIBLE
+ src.stop_fleeing_at = stop_fleeing_at
+ src.start_fleeing_below = start_fleeing_below
+ RegisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_changed))
+ on_health_changed(target)
+
+/datum/element/ai_flee_while_injured/Detach(datum/source)
+ . = ..()
+ UnregisterSignal(source, COMSIG_LIVING_HEALTH_UPDATE)
+
+/// When the mob's health changes, check what the blackboard state should be
+/datum/element/ai_flee_while_injured/proc/on_health_changed(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(isnull(source.ai_controller))
+ return
+
+ var/current_health_percentage = source.health / source.maxHealth
+ if(source.ai_controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
+ if(current_health_percentage > start_fleeing_below)
+ return
+ source.ai_controller.cancel_actions()
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE)
+ return
+
+ if(current_health_percentage < stop_fleeing_at)
+ return
+ source.ai_controller.cancel_actions() // Stop fleeing go back to whatever you were doing
+ source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE)
diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm
new file mode 100644
index 00000000000..837b9747ebc
--- /dev/null
+++ b/code/datums/elements/ai_retaliate.dm
@@ -0,0 +1,27 @@
+/**
+ * Attached to a mob with an AI controller, passes things which have damaged it to a blackboard.
+ * The AI controller is responsible for doing anything with that information.
+ */
+/datum/element/ai_retaliate
+
+/datum/element/ai_retaliate/Attach(datum/target)
+ . = ..()
+ if(!ismob(target))
+ return ELEMENT_INCOMPATIBLE
+
+ target.AddElement(/datum/element/relay_attackers)
+ RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
+
+ ADD_TRAIT(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
+
+/datum/element/ai_retaliate/Detach(datum/source, ...)
+ . = ..()
+ UnregisterSignal(source, COMSIG_ATOM_WAS_ATTACKED)
+
+/// Add an attacking atom to a blackboard list of things which attacked us
+/datum/element/ai_retaliate/proc/on_attacked(mob/victim, atom/attacker)
+ SIGNAL_HANDLER
+
+ if(victim == attacker)
+ return
+ victim.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
diff --git a/code/datums/elements/relay_attackers.dm b/code/datums/elements/relay_attackers.dm
new file mode 100644
index 00000000000..524bb619b8f
--- /dev/null
+++ b/code/datums/elements/relay_attackers.dm
@@ -0,0 +1,95 @@
+/**
+ * This element registers to a shitload of signals which can signify "someone attacked me".
+ * If anyone does it sends a single "someone attacked me" signal containing details about who done it.
+ * This prevents other components and elements from having to register to the same list of a million signals, should be more maintainable in one place.
+ */
+/datum/element/relay_attackers
+
+/datum/element/relay_attackers/Attach(datum/target)
+ . = ..()
+ if(!HAS_TRAIT(target, TRAIT_RELAYING_ATTACKER)) // Little bit gross but we want to just apply this shit from a bunch of places
+ // Boy this sure is a lot of ways to tell us that someone tried to attack us
+ RegisterSignal(target, COMSIG_AFTER_ATTACKED_BY, PROC_REF(on_after_attacked_by))
+ RegisterSignals(target, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW), PROC_REF(on_attack_generic))
+ RegisterSignals(target, list(COMSIG_ATOM_ATTACK_BASIC_MOB, COMSIG_ATOM_ATTACK_ANIMAL), PROC_REF(on_attack_npc))
+ RegisterSignal(target, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act))
+ RegisterSignal(target, COMSIG_ATOM_HITBY, PROC_REF(on_hitby))
+ RegisterSignal(target, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk))
+ ADD_TRAIT(target, TRAIT_RELAYING_ATTACKER, UID())
+
+/datum/element/relay_attackers/Detach(datum/source, ...)
+ . = ..()
+ UnregisterSignal(source, list(
+ COMSIG_AFTER_ATTACKED_BY,
+ COMSIG_ATOM_ATTACK_HAND,
+ COMSIG_ATOM_ATTACK_PAW,
+ COMSIG_ATOM_ATTACK_BASIC_MOB,
+ COMSIG_ATOM_BULLET_ACT,
+ COMSIG_ATOM_HITBY,
+ COMSIG_ATOM_HULK_ATTACK,
+ ))
+ REMOVE_TRAIT(source, TRAIT_RELAYING_ATTACKER, UID())
+
+/datum/element/relay_attackers/proc/on_after_attacked_by(atom/target, obj/item/weapon, mob/attacker, proximity_flag, click_parameters)
+ SIGNAL_HANDLER // COMSIG_AFTER_ATTACKED_BY
+ if(!proximity_flag) // we don't care about someone clicking us with a piece of metal from across the room
+ return
+ if(weapon.force)
+ relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+
+/datum/element/relay_attackers/proc/on_attack_generic(atom/target, mob/living/attacker, list/modifiers)
+ SIGNAL_HANDLER
+
+ // Check for a shove.
+ if(attacker.a_intent == INTENT_DISARM)
+ relay_attacker(target, attacker, ATTACKER_SHOVING)
+ return
+
+ // Else check for harm intent.
+ if(attacker.a_intent == INTENT_HARM)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ return
+
+/datum/element/relay_attackers/proc/on_attack_npc(atom/target, mob/living/attacker)
+ SIGNAL_HANDLER // COMSIG_ATOM_ATTACK_BASIC_MOB + COMSIG_ATOM_ATTACK_ANIMAL
+ // both simple animals and basic mobs have the same var name here but we have to check both types
+ if(isanimal(attacker))
+ var/mob/living/simple_animal/SA = attacker
+ if(SA.melee_damage_upper > 0)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ return
+ if(isbasicmob(attacker))
+ var/mob/living/basic/B = attacker
+ if(B.melee_damage_upper > 0)
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+ return
+
+/// Even if another component blocked this hit, someone still shot at us
+/datum/element/relay_attackers/proc/on_bullet_act(atom/target, obj/item/projectile/hit_projectile)
+ SIGNAL_HANDLER // COMSIG_ATOM_BULLET_ACT
+ if(!hit_projectile.is_hostile_projectile())
+ return
+ if(!ismob(hit_projectile.firer))
+ return
+ relay_attacker(target, hit_projectile.firer, hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+
+/// Even if another component blocked this hit, someone still threw something
+/datum/element/relay_attackers/proc/on_hitby(atom/target, atom/movable/hit_atom, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER // COMSIG_ATOM_HITBY
+ if(!isitem(hit_atom))
+ return
+ var/obj/item/hit_item = hit_atom
+ if(!hit_item.throwforce)
+ return
+ var/mob/thrown_by = locateUID(hit_item.thrownby)
+ if(!ismob(thrown_by))
+ return
+ relay_attacker(target, thrown_by, hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK)
+
+/datum/element/relay_attackers/proc/on_attack_hulk(atom/target, mob/attacker)
+ SIGNAL_HANDLER
+ relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK)
+
+/// Send out a signal identifying whoever just attacked us (usually a mob but sometimes a mech or turret)
+/datum/element/relay_attackers/proc/relay_attacker(atom/victim, atom/attacker, attack_flags)
+ SEND_SIGNAL(victim, COMSIG_ATOM_WAS_ATTACKED, attacker, attack_flags)
diff --git a/code/datums/proximity/ai_target_tracking.dm b/code/datums/proximity/ai_target_tracking.dm
new file mode 100644
index 00000000000..bb47dfec62f
--- /dev/null
+++ b/code/datums/proximity/ai_target_tracking.dm
@@ -0,0 +1,113 @@
+// Proximity monitor that checks to see if anything interesting enters our bounds
+/datum/proximity_monitor/advanced/ai_target_tracking
+ edge_is_a_field = TRUE
+ /// The ai behavior who owns us
+ var/datum/ai_behavior/find_potential_targets/owning_behavior
+ /// The ai controller we're using
+ var/datum/ai_controller/controller
+ /// The target key we're trying to fill
+ var/target_key
+ /// The targeting strategy KEY we're using
+ var/targeting_strategy_key
+ /// The hiding location key we're using
+ var/hiding_location_key
+
+ /// The targeting strategy we're using
+ var/datum/targeting_strategy/filter
+ /// If we've built our field yet
+ /// Prevents wasted work on the first build (since the behavior did it)
+ var/first_build = TRUE
+
+// Initially, run the check manually
+// If that fails, set up a field and have it manage the behavior fully
+/datum/proximity_monitor/advanced/ai_target_tracking/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, datum/ai_behavior/find_potential_targets/owning_behavior, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
+ . = ..()
+ src.owning_behavior = owning_behavior
+ src.controller = controller
+ src.target_key = target_key
+ src.targeting_strategy_key = targeting_strategy_key
+ src.hiding_location_key = hiding_location_key
+ src.filter = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+ RegisterSignal(controller, COMSIG_PARENT_QDELETING, PROC_REF(controller_deleted))
+ RegisterSignal(controller, COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS, PROC_REF(controller_think))
+ RegisterSignal(controller, COMSIG_AI_CONTROLLER_POSSESSED_PAWN, PROC_REF(pawn_changed))
+ RegisterSignal(controller, AI_CONTROLLER_BEHAVIOR_QUEUED(owning_behavior.type), PROC_REF(behavior_requeued))
+ RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_SET(targeting_strategy_key), PROC_REF(targeting_datum_changed))
+ RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_CLEARED(targeting_strategy_key), PROC_REF(targeting_datum_cleared))
+ recalculate_field(full_recalc = TRUE)
+
+/datum/proximity_monitor/advanced/ai_target_tracking/Destroy()
+ . = ..()
+ if(!QDELETED(controller) && owning_behavior)
+ controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller))
+ owning_behavior = null
+ controller = null
+ target_key = null
+ targeting_strategy_key = null
+ hiding_location_key = null
+ filter = null
+
+/datum/proximity_monitor/advanced/ai_target_tracking/recalculate_field(full_recalc = FALSE)
+ . = ..()
+ first_build = FALSE
+
+/datum/proximity_monitor/advanced/ai_target_tracking/setup_field_turf(turf/target)
+ . = ..()
+ if(first_build)
+ return
+ owning_behavior.new_turf_found(target, controller, filter)
+
+/datum/proximity_monitor/advanced/ai_target_tracking/field_turf_crossed(atom/movable/movable, turf/location, turf/old_location)
+ . = ..()
+
+ if(!owning_behavior.atom_allowed(movable, filter, controller.pawn))
+ return
+
+ owning_behavior.new_atoms_found(list(movable), controller, target_key, filter, hiding_location_key)
+
+/// React to controller planning
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_deleted(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// React to the pawn goin byebye
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/pawn_changed(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// React to controller planning
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_think(datum/ai_controller/source, list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors)
+ SIGNAL_HANDLER
+ // If our parent was forgotten, nuke ourselves
+ if(!new_behaviors[owning_behavior])
+ qdel(src)
+
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/behavior_requeued(datum/source, list/new_arguments)
+ SIGNAL_HANDLER
+ check_new_args(arglist(new_arguments))
+
+/// Ensure our args and locals are up to date
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/check_new_args(target_key, targeting_strategy_key, hiding_location_key)
+ var/update_filter = FALSE
+ if(src.target_key != target_key)
+ src.target_key = target_key
+ if(src.targeting_strategy_key != targeting_strategy_key)
+ src.targeting_strategy_key = targeting_strategy_key
+ update_filter = TRUE
+ if(src.hiding_location_key != hiding_location_key)
+ src.hiding_location_key = hiding_location_key
+ if(update_filter)
+ targeting_datum_changed(null)
+
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_changed(datum/source)
+ SIGNAL_HANDLER
+ filter = controller.blackboard[targeting_strategy_key]
+ // Filter changed, need to do a full reparse
+ // Fucking 9 * 9 out here I stg
+ for(var/turf/in_field as anything in field_turfs + edge_turfs)
+ owning_behavior.new_turf_found(in_field, controller, filter)
+
+/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_cleared(datum/source)
+ SIGNAL_HANDLER
+ // Go fuckin home bros
+ qdel(src)
diff --git a/code/game/objects/structures/crates_lockers/crittercrate.dm b/code/game/objects/structures/crates_lockers/crittercrate.dm
index 702614172d0..4f22cfa549d 100644
--- a/code/game/objects/structures/crates_lockers/crittercrate.dm
+++ b/code/game/objects/structures/crates_lockers/crittercrate.dm
@@ -48,7 +48,7 @@
content_mob = pick(/mob/living/simple_animal/pet/dog/corgi,
/mob/living/simple_animal/pet/dog/corgi/lisa,
/mob/living/basic/cow,
- /mob/living/simple_animal/pig,
+ /mob/living/basic/pig,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/turkey,
/mob/living/simple_animal/chick,
@@ -72,7 +72,7 @@
/obj/structure/closet/critter/pig
name = "pig crate"
- content_mob = /mob/living/simple_animal/pig
+ content_mob = /mob/living/basic/pig
/obj/structure/closet/critter/goat
name = "goat crate"
diff --git a/code/modules/mob/living/basic/basic_mob.dm b/code/modules/mob/living/basic/basic_mob.dm
index 5a929e8d349..8ebf272d365 100644
--- a/code/modules/mob/living/basic/basic_mob.dm
+++ b/code/modules/mob/living/basic/basic_mob.dm
@@ -19,6 +19,7 @@ RESTRICT_TYPE(/mob/living/basic)
name = "basic mob"
desc = "If you can see this, make an issue report on GitHub."
healable = TRUE
+ icon = 'icons/mob/animal.dmi'
var/basic_mob_flags
@@ -110,6 +111,9 @@ RESTRICT_TYPE(/mob/living/basic)
// Attack related vars.
// Unclear how much of this should either be componentized or pulled to /mob/living
+ /// Sound played when the critter attacks.
+ var/attack_sound = 'sound/weapons/punch1.ogg'
+ /// Sound played when the critter is attacked.
var/attacked_sound = 'sound/weapons/punch1.ogg'
/// The amount of damage done to the mob when hand-attacked on harm intent.
var/harm_intent_damage = 3
@@ -117,6 +121,20 @@ RESTRICT_TYPE(/mob/living/basic)
var/list/damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
/// Minimum force required to deal any damage
var/force_threshold = 0
+ /// Lower bound of damage done by unarmed melee attacks.
+ var/melee_damage_lower = 0
+ /// Upper bound of damage done by unarmed melee attacks.
+ var/melee_damage_upper = 0
+ /// How much damage this simple animal does to objects, if any
+ var/obj_damage = 0
+ /// Flat armour reduction, occurs after percentage armour penetration.
+ var/armour_penetration_flat = 0
+ /// Percentage armour reduction, happens before flat armour reduction.
+ var/armour_penetration_percentage = 0
+ /// Damage type of a simple mob's melee attack, should it do damage.
+ var/melee_damage_type = BRUTE
+ /// How often can you melee attack?
+ var/melee_attack_cooldown = 2 SECONDS
/mob/living/basic/Initialize(mapload)
. = ..()
@@ -164,6 +182,25 @@ RESTRICT_TYPE(/mob/living/basic)
/mob/living/basic/proc/valid_respawn_target_for(mob/user)
return FALSE
+/mob/living/basic/resolve_unarmed_attack(atom/attack_target, list/modifiers)
+ melee_attack(attack_target, modifiers)
+
+/mob/living/basic/proc/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
+ if(!early_melee_attack(target, modifiers, ignore_cooldown))
+ return FALSE
+
+ var/result = target.attack_basic_mob(src, modifiers)
+ SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result)
+ return result
+
+/mob/living/basic/proc/early_melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
+ face_atom(target)
+ if(!ignore_cooldown)
+ changeNext_move(melee_attack_cooldown)
+ if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target, Adjacent(target), modifiers) & COMPONENT_HOSTILE_NO_ATTACK)
+ return FALSE
+ return TRUE
+
/mob/living/basic/handle_environment(datum/gas_mixture/readonly_environment)
SEND_SIGNAL(src, COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT, readonly_environment)
diff --git a/code/modules/mob/living/basic/cow/cow.dm b/code/modules/mob/living/basic/cow.dm
similarity index 78%
rename from code/modules/mob/living/basic/cow/cow.dm
rename to code/modules/mob/living/basic/cow.dm
index 7715db88b53..1ba8bc4049c 100644
--- a/code/modules/mob/living/basic/cow/cow.dm
+++ b/code/modules/mob/living/basic/cow.dm
@@ -1,7 +1,30 @@
+/datum/ai_planning_subtree/random_speech/cow
+ speech_chance = 2
+ speak = list("Moo?", "Moo", "MOOOOOO")
+ speak_verbs = list("moos", "moos hauntingly")
+ sound = list('sound/creatures/cow.ogg')
+ emote_hear = list("brays.")
+ emote_see = list("shakes her head.")
+
+/datum/ai_controller/basic_controller/cow
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_BASIC_MOB_TIP_REACTING = FALSE,
+ BB_BASIC_MOB_TIPPER = null,
+ )
+
+ ai_traits = AI_FLAG_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,
+ /datum/ai_planning_subtree/random_speech/cow,
+ )
+
/mob/living/basic/cow
name = "cow"
desc = "Known for their milk, just don't tip them over."
- icon = 'icons/mob/animal.dmi'
icon_state = "cow"
icon_living = "cow"
icon_dead = "cow_dead"
diff --git a/code/modules/mob/living/basic/cow/cow_ai.dm b/code/modules/mob/living/basic/cow/cow_ai.dm
deleted file mode 100644
index 34e26e57c9a..00000000000
--- a/code/modules/mob/living/basic/cow/cow_ai.dm
+++ /dev/null
@@ -1,15 +0,0 @@
-/datum/ai_controller/basic_controller/cow
- blackboard = list(
- BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
- BB_BASIC_MOB_TIP_REACTING = FALSE,
- BB_BASIC_MOB_TIPPER = null,
- )
-
- ai_traits = AI_FLAG_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,
- /datum/ai_planning_subtree/random_speech/cow,
- )
diff --git a/code/modules/mob/living/basic/pig.dm b/code/modules/mob/living/basic/pig.dm
new file mode 100644
index 00000000000..09b3f378c32
--- /dev/null
+++ b/code/modules/mob/living/basic/pig.dm
@@ -0,0 +1,50 @@
+/datum/ai_planning_subtree/random_speech/pig
+ speech_chance = 2
+ speak = list("oink?", "oink", "OINK")
+ sound = list(
+ 'sound/creatures/pig/pig1.ogg',
+ 'sound/creatures/pig/pig2.ogg'
+ )
+ emote_hear = list("oinks.")
+ emote_see = list("rolls around.")
+
+/datum/ai_controller/basic_controller/pig
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ )
+
+ ai_traits = AI_FLAG_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/find_nearest_thing_which_attacked_me_to_flee,
+ /datum/ai_planning_subtree/flee_target,
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/random_speech/pig,
+ )
+
+/mob/living/basic/pig
+ name = "pig"
+ desc = "Oink oink."
+ icon_state = "pig"
+ icon_living = "pig"
+ icon_dead = "pig_dead"
+ ai_controller = /datum/ai_controller/basic_controller/pig
+ see_in_dark = 6
+ butcher_results = list(/obj/item/food/meat/ham = 6)
+ health = 50
+ maxHealth = 50
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ mob_biotypes = MOB_ORGANIC | MOB_BEAST
+ gold_core_spawnable = FRIENDLY_SPAWN
+ blood_volume = BLOOD_VOLUME_NORMAL
+ unintelligble_phrases = list("Oink?", "Oink", "OINK")
+ unintelligble_speak_verbs = list("oinks")
+
+/mob/living/basic/pig/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/ai_flee_while_injured)
+ AddComponent(/datum/component/footstep, FOOTSTEP_MOB_SHOE)
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 128ecc4c037..2dbea5e84da 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -717,6 +717,21 @@ emp_act
apply_damage(damage, M.melee_damage_type, affecting, armor)
updatehealth("animal attack")
+/mob/living/carbon/human/handle_basic_attack(mob/living/basic/attacker, modifiers)
+ . = ..()
+ if(.)
+ var/damage = rand(attacker.melee_damage_lower, attacker.melee_damage_upper)
+ if(check_shields(attacker, damage, "[attacker.name]", MELEE_ATTACK, attacker.armour_penetration_flat, attacker.armour_penetration_percentage))
+ return FALSE
+ var/dam_zone = pick("head", "chest", "groin", "l_arm", "l_hand", "r_arm", "r_hand", "l_leg", "l_foot", "r_leg", "r_foot")
+ var/obj/item/organ/external/affecting = get_organ(ran_zone(dam_zone))
+ if(!affecting)
+ affecting = get_organ("chest")
+ affecting.add_autopsy_data(attacker.name, damage) // Add the mob's name to the autopsy data
+ var/armor = run_armor_check(affecting, MELEE, armour_penetration_flat = attacker.armour_penetration_flat, armour_penetration_percentage = attacker.armour_penetration_percentage)
+ apply_damage(damage, attacker.melee_damage_type, affecting, armor)
+ updatehealth("basicmob attack")
+
/mob/living/carbon/human/attack_slime(mob/living/simple_animal/slime/M)
if(..()) //successful slime attack
var/damage = rand(5, 25)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index e12a1b36fb9..f1566e0a230 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -339,6 +339,8 @@
return TRUE
/mob/living/attack_animal(mob/living/simple_animal/M)
+ . = ..()
+
M.face_atom(src)
if((M.a_intent == INTENT_HELP && M.ckey) || M.melee_damage_upper == 0)
M.custom_emote(EMOTE_VISIBLE, "[M.friendly] [src].")
@@ -355,6 +357,24 @@
add_attack_logs(M, src, "Animal attacked")
return TRUE
+// TODO: Probably a bunch of this shit doesn't need to be here but I don't feel
+// like sorting it out right now
+/mob/living/handle_basic_attack(mob/living/basic/attacker, modifiers)
+ if((attacker.a_intent == INTENT_HELP && attacker.ckey) || attacker.melee_damage_upper == 0)
+ attacker.custom_emote(EMOTE_VISIBLE, "[attacker.friendly_verb_continuous] [src].")
+ return FALSE
+ if(HAS_TRAIT(attacker, TRAIT_PACIFISM))
+ to_chat(attacker, "You don't want to hurt anyone!")
+ return FALSE
+
+ if(attacker.attack_sound)
+ playsound(loc, attacker.attack_sound, 50, TRUE, 1)
+ attacker.do_attack_animation(src)
+ visible_message("[attacker] [attacker.attack_verb_continuous] [src]!", \
+ "[attacker] [attacker.attack_verb_continuous] [src]!")
+ add_attack_logs(attacker, src, "Basicmob attacked")
+ return TRUE
+
/mob/living/attack_larva(mob/living/carbon/alien/larva/L)
switch(L.a_intent)
if(INTENT_HELP)
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 3690c0c2618..54837024ea3 100644
--- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
+++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
@@ -305,32 +305,6 @@ GLOBAL_VAR_INIT(chicken_count, 0)
/mob/living/simple_animal/chicken/featherbottom/npc_safe(mob/user)
return FALSE
-/mob/living/simple_animal/pig
- name = "pig"
- desc = "Oink oink."
- icon_state = "pig"
- icon_living = "pig"
- icon_dead = "pig_dead"
- speak = list("oink?","oink","OINK")
- speak_emote = list("oinks")
-// emote_hear = list("brays")
- emote_see = list("rolls around")
- speak_chance = 1
- turns_per_move = 5
- see_in_dark = 6
- butcher_results = list(/obj/item/food/meat/ham = 6)
- response_help = "pets"
- response_disarm = "gently pushes aside"
- response_harm = "kicks"
- attacktext = "kicks"
- health = 50
- maxHealth = 50
- can_collar = TRUE
- mob_biotypes = MOB_ORGANIC | MOB_BEAST
- gold_core_spawnable = FRIENDLY_SPAWN
- blood_volume = BLOOD_VOLUME_NORMAL
- footstep_type = FOOTSTEP_MOB_SHOE
-
/mob/living/simple_animal/turkey
name = "turkey"
desc = "Benjamin Franklin would be proud."
diff --git a/code/modules/projectiles/projectile_base.dm b/code/modules/projectiles/projectile_base.dm
index 49754de3ac1..fa6f81aac3b 100644
--- a/code/modules/projectiles/projectile_base.dm
+++ b/code/modules/projectiles/projectile_base.dm
@@ -593,5 +593,22 @@
/obj/item/projectile/proc/cleanup_beam_segments()
QDEL_LIST_ASSOC(beam_segments)
+/**
+ * Is this projectile considered "hostile"?
+ *
+ * By default all projectiles which deal damage or impart crowd control effects (including stamina) are hostile
+ *
+ * This is NOT used for pacifist checks, that's handled by [/obj/item/ammo_casing/var/harmful]
+ * This is used in places such as AI responses to determine if they're being threatened or not (among other places)
+ */
+/obj/item/projectile/proc/is_hostile_projectile()
+ if(damage > 0 || stamina > 0)
+ return TRUE
+
+ if(stun + weaken + paralyze + knockdown > 0 SECONDS)
+ return TRUE
+
+ return FALSE
+
#undef MOVES_HITSCAN
#undef MUZZLE_EFFECT_PIXEL_INCREMENT
diff --git a/code/tests/game_tests.dm b/code/tests/game_tests.dm
index e7bd5315b61..03f191b9e13 100644
--- a/code/tests/game_tests.dm
+++ b/code/tests/game_tests.dm
@@ -29,6 +29,7 @@
#include "test_dynamic_budget.dm"
#include "test_elements.dm"
#include "test_emotes.dm"
+#include "test_ensure_subtree_operational_datum.dm"
#include "test_init_sanity.dm"
#include "test_log_format.dm"
#include "test_map_templates.dm"
diff --git a/code/tests/test_ensure_subtree_operational_datum.dm b/code/tests/test_ensure_subtree_operational_datum.dm
new file mode 100644
index 00000000000..3b6cbf6b836
--- /dev/null
+++ b/code/tests/test_ensure_subtree_operational_datum.dm
@@ -0,0 +1,64 @@
+/// The subtree that requires the operational datum.
+#define REQUIRED_SUBTREE "required_subtree"
+/// The list of typepaths of applicable operational datums that would satisfy the requirement.
+#define REQUIRED_OPERATIONAL_DATUMS "required_operational_datums"
+
+/// Unit Test that ensure that if we add a specific planning subtree to a basic mob's planning tree, that we also have the operational datum needed for it (component/element).
+/// This can be extended to other "mandatory" operational datums for certain subtrees to work.
+/datum/game_test/ensure_subtree_operational_datum
+ /// Associated list of mobs that we need to test this on. Key is the typepath of the mob, value is a list of the planning subtree and the operational datums that are required for it.
+ var/list/testable_mobs = list()
+
+/datum/game_test/ensure_subtree_operational_datum/Run()
+ gather_testable_mobs()
+ test_applicable_mobs()
+
+/// First, look for all mobs that have a planning subtree that requires an element, then add it to the list for stuff to test afterwards. Done like this to not have one mumbo proc that's hard to read.
+/datum/game_test/ensure_subtree_operational_datum/proc/gather_testable_mobs()
+ for(var/mob/living/basic/checkable_mob as anything in subtypesof(/mob/living/basic))
+ var/datum/ai_controller/testable_controller = initial(checkable_mob.ai_controller)
+ if(isnull(testable_controller))
+ continue
+
+ // we can't do inital() memes on lists so it's allocation time
+ testable_controller = allocate(testable_controller)
+ var/list/ai_planning_subtrees = testable_controller.planning_subtrees // list of instantiated datums. easy money
+ if(!length(ai_planning_subtrees))
+ continue
+
+ for(var/datum/ai_planning_subtree/testable_subtree as anything in ai_planning_subtrees)
+ var/list/necessary_datums = testable_subtree.operational_datums
+ if(isnull(necessary_datums))
+ continue
+
+ testable_mobs[checkable_mob] = list(
+ REQUIRED_OPERATIONAL_DATUMS = necessary_datums,
+ REQUIRED_SUBTREE = testable_subtree.type,
+ )
+
+/// Then, test the mobs that we've found
+/datum/game_test/ensure_subtree_operational_datum/proc/test_applicable_mobs()
+ for(var/mob/living/basic/checkable_mob as anything in testable_mobs)
+ var/list/checkable_mob_data = testable_mobs[checkable_mob]
+ checkable_mob = allocate(checkable_mob)
+
+ var/datum/ai_planning_subtree/test_subtree = checkable_mob_data[REQUIRED_SUBTREE]
+ var/list/trait_sources = GET_TRAIT_SOURCES(checkable_mob, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM)
+ if(!length(trait_sources)) // yes yes we could use `COUNT_TRAIT_SOURCES` but why invoke the same macro twice
+ TEST_FAIL("The mob [checkable_mob] ([checkable_mob.type]) does not have ANY instances of TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, but has a planning subtree ([test_subtree]) that requires it!")
+ continue
+
+ var/has_element = FALSE
+ var/list/testable_operational_datums = checkable_mob_data[REQUIRED_OPERATIONAL_DATUMS]
+ for(var/iterable in trait_sources)
+ if(iterable in testable_operational_datums)
+ has_element = TRUE
+ break
+
+ if(!has_element)
+ var/list/message_list = list("The mob [checkable_mob] ([checkable_mob.type]) has a planning subtree ([test_subtree]) that requires a component/element, but does not have any!")
+ message_list += "Needs one of the following to satisfy the requirement: ([testable_operational_datums.Join(", ")])"
+ TEST_FAIL(message_list.Join(" "))
+
+#undef REQUIRED_SUBTREE
+#undef REQUIRED_OPERATIONAL_DATUMS
diff --git a/paradise.dme b/paradise.dme
index 96d6995744a..627db15476a 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -445,10 +445,18 @@
#include "code\datums\ai\ai_controller.dm"
#include "code\datums\ai\ai_planning_subtree.dm"
#include "code\datums\ai\basic_mobs\basic_controller.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\interact_with_target.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targeting.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm"
+#include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeting.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\random_speech.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_nearest_target_to_flee.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm"
#include "code\datums\ai\generic\find_and_set.dm"
#include "code\datums\ai\generic\generic_behaviors.dm"
@@ -464,6 +472,7 @@
#include "code\datums\cache\crew.dm"
#include "code\datums\cache\powermonitor.dm"
#include "code\datums\components\_component.dm"
+#include "code\datums\components\ai_retaliate_advanced.dm"
#include "code\datums\components\anti_magic.dm"
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\boss_music.dm"
@@ -574,6 +583,8 @@
#include "code\datums\diseases\advance\symptoms\weight.dm"
#include "code\datums\diseases\advance\symptoms\youth.dm"
#include "code\datums\elements\_element.dm"
+#include "code\datums\elements\ai_flee_while_injured.dm"
+#include "code\datums\elements\ai_retaliate.dm"
#include "code\datums\elements\atmos_requirements.dm"
#include "code\datums\elements\basic_eating.dm"
#include "code\datums\elements\body_temperature.dm"
@@ -584,6 +595,7 @@
#include "code\datums\elements\earhealing.dm"
#include "code\datums\elements\high_value_item.dm"
#include "code\datums\elements\rad_insulation.dm"
+#include "code\datums\elements\relay_attackers.dm"
#include "code\datums\elements\ridable.dm"
#include "code\datums\elements\rust_element.dm"
#include "code\datums\elements\shatters_when_thrown.dm"
@@ -619,6 +631,7 @@
#include "code\datums\outfits\plasmamen_outfits.dm"
#include "code\datums\outfits\vv_outfit.dm"
#include "code\datums\proximity\advanced_proximity_monitor.dm"
+#include "code\datums\proximity\ai_target_tracking.dm"
#include "code\datums\proximity\proximity_monitor.dm"
#include "code\datums\proximity\singulo_proximity_monitor.dm"
#include "code\datums\ruins\lavaland.dm"
@@ -2327,8 +2340,8 @@
#include "code\modules\mob\living\stat_states.dm"
#include "code\modules\mob\living\taste.dm"
#include "code\modules\mob\living\basic\basic_mob.dm"
-#include "code\modules\mob\living\basic\cow\cow.dm"
-#include "code\modules\mob\living\basic\cow\cow_ai.dm"
+#include "code\modules\mob\living\basic\pig.dm"
+#include "code\modules\mob\living\basic\cow.dm"
#include "code\modules\mob\living\brain\brain_death.dm"
#include "code\modules\mob\living\brain\brain_emote.dm"
#include "code\modules\mob\living\brain\brain_life.dm"
diff --git a/sound/creatures/pig/pig1.ogg b/sound/creatures/pig/pig1.ogg
new file mode 100644
index 00000000000..eb63bd5c6eb
Binary files /dev/null and b/sound/creatures/pig/pig1.ogg differ
diff --git a/sound/creatures/pig/pig2.ogg b/sound/creatures/pig/pig2.ogg
new file mode 100644
index 00000000000..5a2ee7e0639
Binary files /dev/null and b/sound/creatures/pig/pig2.ogg differ
diff --git a/tools/UpdatePaths/Scripts/28987_pig.txt b/tools/UpdatePaths/Scripts/28987_pig.txt
new file mode 100644
index 00000000000..77a3c75aaab
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/28987_pig.txt
@@ -0,0 +1 @@
+/mob/living/simple_animal/pig/@SUBTYPES : /mob/living/basic/pig/@SUBTYPES{@OLD}